I have a question regarding finding and detecting gaps per row in VBA excel.
The dificult part is gaps that start on Friday and run until Thursday (FTGaps) need to be added up seperately and gaps starting on other days have to be counted seperately.
So for instance in the picture the result from the first row has to be: 1 FTGap and 1 Gap
And the second row has to be 1 FTGap.
But if there is a empty cell earlier in the row it has to be counted as a different gap.
So for instance the following rows output is 2 gaps and 1 FTGap.
I hope my question is clear.
Thanks in advance
What I tried
For Row = 3 To Worksheets("Kalender2").UsedRange.Rows.Count
GapDays = 0
FTGapDays = 0
For col = 2 To 55 'Worksheets("Kalender2").Cells(2,
Columns.Count).End(xlToLeft).Column
If Worksheets("Kalender2").Cells(Row, col) = "0" And
Worksheets("Kalender2").Cells(2, col).Value = "Friday" Then
FTGapDays = FTGapDays + 1
ElseIf Worksheets("Kalender2").Cells(Row, col) = "0" And
FTGapDays <> 0 Then
FTGapDays = FTGapDays + 1 'doortellen gap startend op
vrijdag
ElseIf Worksheets("Kalender2").Cells(Row, col) = "0" And
FTGapDays = 0 Then 'And Worksheets("Kalender2").Cells(2, Col).Value <>
"Friday" Then
GapDays = GapDays + 1 'eerste lege cel andere dag dan
vrijdag
End If
Next col
If col = 54 Then
Call EndGap
End If
Call EndGap
Next Row
'
And then the second Sub Endgap():
If FTGapDays <> 0 Then
If FTGapDays < 7 Then
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays >= 7 And FTGapDays < 14 Then
FTGaps = FTGaps + 1
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays >= 14 And FTGapDays < 21 Then
FTGaps = FTGaps + 2
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays >= 21 And FTGapDays < 28 Then
FTGaps = FTGaps + 3
LegGaps = LegGaps + 1
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays >= 28 And FTGapDays < 35 Then
FTGaps = FTGaps + 4
LegGaps = LegGaps + 1
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays >= 35 And FTGapDay < 42 Then
FTGaps = FTGaps + 5
LegGaps = LegGaps + 1
If GapDays = 0 Then
Gaps = Gaps + 1
End If
ElseIf FTGapDays = 42 Then
FTGaps = FTGaps + 6
LegGaps = LegGaps + 2
End If
End If
End Sub
Please, test the next solution. It uses a kind of trick: Dividing a number to 0 in a formula will return an error. So, such a formula is placed two rows down after the last, then using SpecialCells(xlCellTypeFormulas, xlErrors) creates a discontinuous range of the gaps and process it. The processing result is returned two columns to the right of the last column. In the first such a column the 'Gaps' and in the second one 'FTGap'. The code assumes that the row keeping the days name is the second and the zero (0) seen in your pictures are values not string as you tried using in your code:
Sub extractGaps()
Dim sh As Worksheet, lastR As Long, lastCol As Long, rng As Range, arrCount, arrRows, i As Long
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
If sh.Range("A" & lastR).HasFormula Then
If left(sh.Range("A" & lastR).Formula, 3) = "=1/" Then
sh.rows(lastR).EntireRow.ClearContents
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
End If
End If
lastCol = sh.cells(2, sh.Columns.count).End(xlToLeft).column
ReDim arrCount(1 To lastR - 2, 1 To lastCol)
Application.Calculation = xlCalculationManual: Application.ScreenUpdating = False
For i = 3 To lastR
arrRows = countGaps(sh.Range("A" & i, sh.cells(i, lastCol)), lastR, lastCol)
arrCount(i - 2, 1) = arrRows(0): arrCount(i - 2, 2) = arrRows(1)
Next i
sh.Range("A" & lastR + 2).EntireRow.ClearContents
sh.cells(3, lastCol + 2).Resize(UBound(arrCount), 2).value = arrCount
Application.Calculation = xlCalculationAutomatic: Application.ScreenUpdating = True
MsgBox "Ready..."
End Sub
Function countGaps(rngR As Range, lastR As Long, lastCol As Long) As Variant
Dim sh As Worksheet: Set sh = rngR.Parent
Dim rngProc As Range, i As Long, A As Range, FTGap As Long, Gap As Long
Dim boolGap As Boolean, bigGaps As Double, mtchFr
Set rngProc = sh.Range(sh.cells(lastR + 2, 1), sh.cells(lastR + 2, lastCol)) 'a range where to place a formula returnig errors deviding by 0...
rngProc.Formula = "=1/" & rngR.Address
On Error Resume Next
Set rngProc = rngProc.SpecialCells(xlCellTypeFormulas, xlErrors)
On Error GoTo 0
If rngProc.cells.count = rngR.cells.count Then
If IsNumeric(rngProc.cells(1)) Then countGaps = Array(0, 0): Exit Function
End If
If rngProc Is Nothing Then countGaps = Array(0, 0): Exit Function 'in case of no gaps...
Gap = 0: FTGap = 0: Debug.Print rngProc.Areas.count
For Each A In rngProc.Areas
Debug.Print sh.Range(sh.cells(2, A.cells(1).column), sh.cells(2, A.cells(A.Columns.count).column)).Address: 'Stop
If A.cells.count < 7 Then
Gap = Gap + 1
ElseIf A.cells.count = 7 Then
If sh.cells(2, A.cells(1).column).value = "Friday" Then
FTGap = FTGap + 1
Else
Gap = Gap + 1
End If
Else 'for more than 7 empty cells:
'set the Friday day position in the empty cells range/area:
mtchFr = Application.match("Friday", sh.Range(sh.cells(2, A.cells(1).column), sh.cells(2, A.cells(A.Columns.count).column)), 0)
If A.Columns.count - mtchFr < 6 Then 'no any FTGap existing:
Gap = Gap + 1
ElseIf A.cells.count - mtchFr = 6 Then 'fix a FTGap
Gap = Gap + 1: FTGap = FTGap + 1
Else
bigGaps = ((A.cells.count - mtchFr) - i + 1) / 7
FTGap = FTGap + Int(bigGaps)
If mtchFr <> 1 Then Gap = Gap + 1 'the gap before Friday
If A.cells.count - i + 1 - Int(bigGaps) * 7 > 0 Then Gap = Gap + 1
End If
End If
Next A
countGaps = Array(Gap, FTGap)
End Function
Problem
We have a range (apparently "B3:BC" & UsedRange.Rows.count). The range is preceded by a row (B2:BC2) containing days of the week repeated in consecutively order: Monday, Tuesday, etc.
Cells for each row in the range contain either a 0 or some other value (integer? does not matter much). Consecutive 0's in a row (length > 0) are treated as a gap. We have two types of gaps:
a regular Gap: a range of consecutive 0's of any length > 0;
a Friday-through-to-Thursday-Gap (FtGap): a range of consecutive 0's, that starts on a Friday and ends on Thursday (length = 7).
For each row we want to count the number of Gaps and FtGaps, taking into account the following condition: a range of consecutive 0's that qualifies as a FtGap should not also be counted as a regular Gap.
Solution
To solve this problem, I've used range B3:BC20 for the data. Cells in this range have been populated randomly with either 0's or 1's (but this value could be anything) using =IF(RAND()>0.8,0,1). My row with "days of the week" starts with a "Monday", but this should make no difference.
I've used the following method:
Create two arrays for row days and the data.
Loop through each row array with nested loop through cols to access all cells per row.
On each new 0, increment the total Gap count (GapTrack) by 1. For each new 0, increment a variable (GapTemp) by 1 that tracks the length of the Gap. Reset GapTemp on the next non-0.
For each 0 on a "Friday", start incrementing a variable FtTemp. We keep checking if its value (length) has reached any multiple of 7. When it does, we increment the Ft count (FtTrack) by 1.
On each new non-0, check if FtTemp mod 7 = 0 and GapTemp Mod 7 = 0 and GapTemp > 0. If True, we will have added a regular Gap to our total count of the same length as one or more FtTemps. This violates the condition mentioned above. Remedy this by decrementing GapTrack by 1.
At the end of the row, we wrap GapTrack and FtTrack inside an array, assign it to a new key in a dictionary. At the start of the next row, we reset all our variables, and restart the count.
When the loop is finished, we end up with a dictionary that contains all our counts per row. We write this data away somewhere.
Code as follows with some further notes on what is happening. N.B. I've used "Option Explicit" to force proper declaration of all our variables.
Option Explicit
Sub CountGaps()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ActiveWorkbook
Set ws = wb.Worksheets("Kalender2")
Dim rngDays As Range, rngData As Range
Dim arrDays As Variant, arrData() As Variant
Set rngDays = ws.Range("Days") 'Named range referencing $B$2:$BC$2 in "Kalender2!"
Set rngData = ws.Range("Data") 'Named range referencing $B$3:$BC$20 in "Kalender2!"
'populate arrays with range values
arrDays = rngDays.Value 'dimensions: arrDays(1, 1) to arrDays(1, rngDays.Columns.Count)
arrData = rngData.Value 'dimensions: arrData(1, 1) to arrData(rngData.rows.Count, rngData.Columns.Count)
'declare ints for loop through rows (i) and cols (i) of arrData
Dim i As Integer, j As Integer
'declare booleans to track if we are inside a Gap / FtGap
Dim GapFlag As Boolean, FtFlag As Boolean
'declare ints to track current Gap count (GapTemp), sum Gap count (GapTrack), and same for Ft
Dim GapTemp As Integer, GapTrack As Integer, FtTemp As Integer, FtTrack As Integer
'declare dictionary to store GapTrack and FtTrack for each row
'N.B. in VBA editor (Alt + F11) go to Tools -> References, add "Microsoft Scripting Runtime" for this to work
Dim dict As New Scripting.Dictionary
'declare int (counter) for iteration over range to fill with results
Dim counter As Integer
'declare key for loop through dict
Dim key As Variant
'-----
'start procedure: looping through arrData rows: (arrData(i,1))
For i = LBound(arrData, 1) To UBound(arrData, 1)
'for each new row, reset variables to 0/False
GapTemp = 0
GapTrack = 0
GapFlag = False
FtTemp = 0
FtTrack = 0
FtFlag = False
'nested loop through arrData columns: (arrData(i,2))
For j = LBound(arrData, 2) To UBound(arrData, 2)
If arrData(i, j) = 0 Then
'cell contains 0: do stuff
If arrDays(1, j) = "Friday" Then
'Day = "Friday", start checking length Ft gap
FtFlag = True
End If
'increment Gap count
GapTemp = GapTemp + 1
If GapFlag = False Then
'False: Gap was not yet added to Total Gap count;
'do this now
GapTrack = GapTrack + 1
'toggle Flag to ensure continuance of 0 range will not be processed anew
GapFlag = True
End If
If FtFlag Then
'We are inside a 0 range that had a Friday in the preceding cells
'increment Ft count
FtTemp = FtTemp + 1
If FtTemp Mod 7 = 0 Then
'if True, we will have found a new Ft Gap, add to Total Ft count
FtTrack = FtTrack + 1
'toggle Flag to reset search for new Ft Gap
FtFlag = False
End If
End If
Else
'cell contains 1: evaluate variables
If (FtTemp Mod 7 = 0 And GapTemp Mod 7 = 0) And GapTemp > 0 Then
'if True, then it turns out that our last range STARTED with a "Friday" and continued through to a "Thursday"
'if so, we only want to add this gap to the Total Ft count, NOT to the Total Gap count
'N.B. since, in fact, we will already have added this range to the Total Gap count, we need to retract that step
'Hence: we decrement Total Gap count
GapTrack = GapTrack - 1
End If
'since cell contains 1, we need to reset our variables again (except of course the totals)
GapTemp = 0
GapFlag = False
FtTemp = 0
FtFlag = False
End If
Next j
'finally, at the end of each row, we assign the Total Gap / Ft counts as an array to a new key (i = row) in our dictionary
dict.Add i, Array(GapTrack, FtTrack)
Next i
'we have all our data now stored in the dictionary
'example of how we might write this data away in a range:
rngDays.Columns(rngData.Columns.Count).Offset(0, 1) = "Gaps" 'first col to the right of data
rngDays.Columns(rngData.Columns.Count).Offset(0, 2) = "FtGaps" 'second col to the right of data
'set counter for loop through keys
counter = 0
For Each key In dict.Keys
'resize each cell in first col to right of data to fit "Array(GapTrack, FtTrack)" and assign that array to its value ("dict(key)")
rngData.Columns(rngData.Columns.Count).Offset(counter, 1).Resize(1, 2).Value = dict(key)
'increment counter for next cell
counter = counter + 1
Next key
End Sub
Snippet of result:
Let me know if you experience any difficulties with implemention.
Related
I´ve an Excel file with 10 Columns. In columns 2, 3, 4 I have a number or a dash.
If the sum of these 3 cells is greater than 1, I need to replace that entire row with n rows that have only one of the columns with the value 1 but the other cells stay the same.
Example
1 - - #-> leave it as is
- 2 - #-> replace that row with 2 rows : - 1 - ; - 1 -
2 - 1 #-> replace that row with 3 rows : 1 - - ; 1 - - ; - - 1;
I managed to iterate from bottom up, but I´m having trouble storing a row in memory, manipulate it and insert below.
Sub Test()
Dim rng As Range
Dim count20, count40, count45, total, i As Integer
Set rng = Range("A3", Range("A3").End(xlDown))
For i = rng.Cells.count To 1 Step -1
count20 = 0
count40 = 0
count45 = 0
total = 0
count20 = Cells(rng.Item(i).Row, 10).Value
If count20 > 1 Then
total = total + count20
End If
count40 = Cells(rng.Item(i).Row, 11).Value
If count40 > 1 Then
total = total + count40
End If
count45 = Cells(rng.Item(i).Row, 12).Value
If count45 > 1 Then
total = total + count45
End If
If total <> 0 Then
MsgBox total
End If
Next i
End Sub
EDIT 2
I’ve provided alternative code based on your latest comment. It uses columns J-L (10-12) as the numeric cells to be changed, and columns A-I (1-9) and M-AD (13-30) as the cells with text to be preserved. As before, sheet 1 starting in row 3 is assumed, and you can change this to whatever you need.
Option Explicit
Sub testJtoL()
Dim LastRow As Long, i As Long, j As Long, c As Long, _
insertR As Long, TopRow As Long, BottomRow As Long
Dim b As Range
Dim ws As Worksheet
'*** This code is based your values being in Columns J-L (10-12) in sheet 1 ***
'Set sheet 1 as ws
Set ws = Sheet1
'Sheet1 column J is used here to get your last row
LastRow = ws.Cells(Rows.Count, 10).End(xlUp).Row
'*** This code is based your values starting in Row 3 ***
For c = LastRow To 3 Step -1
'Determine number of rows to insert based on sum of that row
insertR = Application.WorksheetFunction.Sum(Range(Cells(c, 10), Cells(c, 12))) - 1
If insertR = 0 Then GoTo skip
'STEP 1 insert the correct number of rows
With ws.Range(Cells(c + 1, 1), Cells(c + insertR, 30))
.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
End With
'STEP 2 fill the values into the correct number of rows
insertR = insertR + 1
With ws.Range(Cells(c, 1), Cells(c, 30))
.Resize(insertR, 30).Value = .Value
End With
TopRow = c
If insertR = 0 And c = 3 Then
BottomRow = c
Else
BottomRow = c + insertR - 1
End If
'STEP 3 replace all numbers with 1 or "-"
'Replace numbers in column J
If ws.Range(Cells(c, 10), Cells(c, 10)).Value = "-" Then GoTo SkipA
i = ws.Range(Cells(c, 10), Cells(c, 10)).Value
j = 1
For Each b In ws.Range(Cells(TopRow, 10), Cells(BottomRow, 10))
If j <= i Then
b.Value = 1
b.Offset(0, 1).Value = "-"
b.Offset(0, 2).Value = "-"
Else
b.Value = "-"
End If
j = j + 1
Next b
SkipA:
'Replace numbers in column K
j = 1
For Each b In ws.Range(Cells(TopRow, 11), Cells(BottomRow, 11))
If b.Value = "-" Then GoTo SkipB
i = b.Value
If j <= i Then
b.Value = 1
b.Offset(0, 1).Value = "-"
Else
b.Value = "-"
End If
j = j + 1
SkipB:
Next b
'Replace numbers in column L
j = 1
For Each b In ws.Range(Cells(TopRow, 12), Cells(BottomRow, 12))
If b.Value = "-" Then GoTo SkipC
i = b.Value
If j <= i Then
b.Value = 1
Else
b.Value = "-"
End If
j = j + 1
SkipC:
Next b
skip:
Next c
End Sub
I have the following code, that is doing some if's, but with the values I am struggling to see how I create, and add the values to an array. At the moment, I am just adding the values to a listbox
List(lC, 0) = sh1.Cells(row, 23)
I tried creating an integer and, and then used something like
var = var & List(lC, 0) = sh1.Cells(row, 23)
But I am not sure if that's the correct way?
Private Sub CommandButton3_Click()
Dim sh1
Dim LR
Dim lC
Dim row
Me.lstUsedRooms.Clear
Set sh1 = ThisWorkbook.Worksheets(4) 'room order from sheets
With sh1
LR = .Range("A" & .Rows.Count).End(xlUp).row
End With
lC = 0
With Me.lstUsedRooms
.ColumnCount = 1 'there is 8 columns
.RowSource = ""
.ColumnWidths = 40
For row = 2 To LR
NewIVTime = Format("14:00", "h:mm:ss")
If Left(sh1.Cells(row, 6), 10) = "24/05/2019" Then ' Gets all interviews for the date specified
Dim LTime As Date
Dim LTime1 As Date
LTime = Format(sh1.Cells(row, 7), "h:mm:ss") 'Gets the times from all the rooms from the date stated above
LTime1 = CDate(LTime) + 3 / 24 ' Adds 3 hours to the time above
If LTime1 < NewIVTime Then ' Check which interviews display three hours after the new interview
.AddItem
.List(lC, 0) = sh1.Cells(row, 23)
lC = lC + 1
End If
End If
Next
If .ListCount = 0 Then
Me.lstUsedRooms.ColumnWidths = 100
Me.lstUsedRooms.AddItem "No Rooms"
End If
End With
End Sub
First see what you are going to be inputting, if you only need a 1 dimensional array then the best option is a collection:
Dim newCollection as New Collection
For each r in Range
newCollection.Add Value 'Add value here
Next r
If you are needing a multidimensional array then the array function is the best way:
Dim zArray() as variant
Redim zArray(x, y, ...) 'x and y are size of array
Or
Redim Preserve zArray(x, y, ...) 'If you loop through the Redim
For i = 1 to x
For j = 1 to y
zArray(x,y)
Next j
Next i
So I'm trying to compare dates in the chart pictured. I want to compare cells 1 and 2 and if the dates are the same then move to 3 and 4 and do the same comparison and then move to 5 and 6 and so on. If the dates are different I want to add 1 to a counter. Then at the end of each row I need to fill the cell at the end of the row with the 0 in it currently with the counter value and then reset the counter and move to the next row and so on. so the circled counter should read 1 because there is one pair of different dates. The code i have so far is attached. Currently it tells me "Object required" at Set CompD1. Pretty new to this so any help is appreciated.
Dim i As Integer
Dim j As Integer
Dim AdjPln As Integer
Dim CompD1 As Range
Dim CompD2 As Range
Dim cRow As Integer
For i = 0 To 49
AdjPln = 0
cRow = i + 13
For j = 0 To 9
Set CompD1 = Cells(cRow, j + 5).value
Set CompD2 = Cells(cRow, j + 6).value
If CompD1 = CompD2 Then
j = j + 2
Else
AdjPln = AdjPln + 1
j = j + 2
End If
Next j
Cells(cRow, 24) = AdjPln
Stop
Next i
I think your j loop is the issue when you tried to change the value with the formula to increment by 2 instead of 1. When you went to loop by increments other than one, you can use the Step option. In your case, you want to loop j by 2 so Step 2. You can also do negative if that is useful.
See if this works:
Dim i As Integer, j As Integer, AdjPln As Integer, cRow As Integer
For i = 0 To 49
AdjPln = 0
cRow = i + 13
For j = 0 To 9 Step 2
If Cells(cRow, j + 5).Value <> Cells(cRow, j + 6).Value Then
AdjPln = AdjPln + 1
End If
Next j
Cells(cRow, 24) = AdjPln
Next i
It appears that this could be done with a simple formula. Put this in X13 and drag down.
=SUMPRODUCT((E13:U13<>F13:V13)*(E13:U13<>"")*(F13:V13<>"")*ISODD(COLUMN(E13:U13)))
I have challenges in highlighting/copying alternate rows in one column and pasting it to the next column and aligned.
Here's a screenshot:
Following code assumes you have two separate tabs, SRC and DST and the range of data starts in the first cell. Will do all in a single step:
Public Sub CopyAlternate()
Dim i As Long
i = 2
While Len(Sheets("SRC").Cells(i, 1).Value) > 0
Sheets("DST").Cells(i / 2 + 1, 1).Value = Sheets("SRC").Cells(i, 1).Value
Sheets("DST").Cells(i / 2 + 1, 2).Value = Sheets("SRC").Cells(i + 1, 1).Value
i = i + 2
Wend
End Sub
You can take this code and adjust it to taste:
Sub alternate()
Dim i As Integer
Dim j As Integer
Dim n As Integer
i = 0
j = 0
n = 0
With ActiveSheet
For Each c In .Range("A4:A16")
.Cells(20 + j, 1 + i).Value = c.Value
If n = 0 Or n Mod 2 = 0 Then
i = 1
j = j
Else
i = 0
j = j + 1
End If
n = n + 1
Next c
End With
End Sub
This worked for me when rebuilding your example with letters (for faster checking).
I have values which could be visualized as a matrix:
Example:
5 0 0 11 0 0 0 0 0 0 0
15 5 0 0 11 0 0 0 0 0 0
3 11 5 0 0 0 0 0 0 0 0
Colum sums would be:
23 16 5 11 11 0 0 0 0 0 0
Total sum would be: 66
If the sums should be 6 for example in each column filling it up starting from the left side what would be the best way to distribute the numbers in the rows? In the end I would need something like this:
2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2
Colum sums would be:
6 6 6 6 6 6 6 6 6 6 6
Total sum would be: 66
Another example where the sum in the columns does not indicate even distribution:
3 3 3 3 3 3 3 3 2 0 0
3 3 3 3 3 3 3 3 0 0 0
2 2 2 2 2 2 2 2 0 0 0
Colum sums would be:
8 8 8 8 8 8 8 8 2 0 0
Or another example with column value of 10:
4 4 4 4 4 4 2 0 0 0 0
4 4 4 4 4 4 2 0 0 0 0
2 2 2 2 2 2 2 0 0 0 0
Colum sums would be:
10 10 10 10 10 10 6 0 0 0 0
What I have so far is this but it is not working:
For i = 0 To UBound(ColArray) - 1
ExpColMaxDays = CalculatingManDays(ExpRows, ColArray(i))
DiffManDays = ExpColMaxDays - MonthlyMax
DevAmount = DiffManDays
For j = 0 To UBound(RowArray)
If DevAmount < 0 Then
Do While DevAmount < 0
cells(RowArray(j), ColArray(i)).Value = cells(RowArray(j), ColArray(i)).Value + 1
cells(RowArray(j), ColArray(i) + 1).Value = cells(RowArray(j), ColArray(i) + 1).Value - 1
DevAmount = DevAmount + 1
Loop
ElseIf DevAmount > 0 Then
Do While DevAmount > 0
cells(RowArray(j), ColArray(i)).Value = cells(RowArray(j), ColArray(i)).Value - 1
cells(RowArray(j), ColArray(i) + 1).Value = cells(RowArray(j), ColArray(i) + 1).Value + 1
DevAmount = DevAmount - 1
Loop
End If
Next j
Next i
It is difficult to answer your question.
Problem 1
ExpColMaxDays = CalculatingManDays(ExpRows, ColArray(i))
What are CalculatingManDays and ExpRows?
Problem 2
What are RowArray and ColArray? This seems a very complicated way of accessing a block of cells. The following is easier unless there is some significance to this approach I am missing.
For RowCrnt = RowTop To RowBottom
For ColCrnt = ColLeft to ColRight
... Cells(RowCrnt, ColCrnt) ...
Problem 3
If you really just want to distribute the values evenly across the rectangle, I suggest:
Sub Rearrange(RowTop As Long, ColLeft As Long, _
RowBottom As Long, ColRight As Long)
' I assume the cell values are all integers without checking
Dim CellValue As Long
Dim ColCrnt As Long
Dim NumCells As Long
Dim Remainder As Long
Dim RowCrnt As Long
Dim TotalValue As Long
' Calculate the total value
TotalValue = 0
For RowCrnt = RowTop To RowBottom
For ColCrnt = ColLeft To ColRight
TotalValue = TotalValue + Cells(RowCrnt, ColCrnt).Value
Next
Next
' Calculate the standard value for each cell and the remainder which
' will be distributed over the early cells
NumCells = (RowBottom - RowTop + 1) * (ColRight - ColLeft + 1)
CellValue = TotalValue / NumCells
Remainder = TotalValue Mod NumCells
For RowCrnt = RowTop To RowBottom
For ColCrnt = ColLeft To ColRight
If Remainder > 0 Then
Cells(RowCrnt, ColCrnt).Value = CellValue + 1
Remainder = Remainder - 1
Else
Cells(RowCrnt, ColCrnt).Value = CellValue
End If
Next
Next
End Sub
New section in response to respecification of problem
By reading all your questions, I think I have an understanding of what you are attempting. If my understanding is correct, I have had a similar problem.
One of my employers required us to keep records of the time spent on each activity type per project. There were peaks (because we worked evenings and weekends to meet deadlines) and troughs (because we could not progress any of our projects) but the electronic system into which we entered our timesheets required we work not more than 37.5 hours per week. The employer wanted the correct time recorded against each project and activity type so we had to spread the actual time out from peaks to troughs without moving time from one activity type or project to another.
The algorithm I used to spread out my time was as follows:
If the total time for the period was not the required multiple of 37.5, time was moved from the highest peaks or the deepest troughs to the first week of the next period.
Each cycle of the main loop would pick the week with the highest total. If this total was less than or equal to 37.5 hours, the algorithm was finished.
The time recorded against each task (activity type and project) would be reduced so the new total was 37.5 and the new proportion of each task’s time to the total time for the week was as similar to the original proportion as possible.
The time subtracted from each task would be divided equally between the week before and the week after unless that week had already been correctly in which case the next uncorrected week in the same direction received the extra time.
My code does not perform step 1. If the total time exceeds the permitted maximum, the problem is rejected as unsolvable. The result of steps 2 to 4 is not the even spread of your examples because time is moved from a peak to the nearest trough and because time is not moved from row to row. At the end of the process, all peaks have been removed and any remaining troughs can be anywhere within the period. This gives a more realistic appearance and shows how time might have been allocated to tasks if the weekly maximum had not been exceeded.
For testing I have loaded each worksheet with a problem. Cell A1 contains the maximum column value. The matrix starts in cell B2 and continues to the first blank column and the first blank row. The remainder of row 1 and column A may be used for headings if desired. Columns to the right of the first blank column are not examined and may be used for comments. The area below the matrix is used for the answer.
I have a control routine which loads data and calls the redistribution routine which does not know about the worksheets.
The redistribution routine accepts the maximum column value and the matrix as parameters and updates the matrix in situ.
In general I believe in giving the client what they have asked for. I may gently push them in the direction of what I think they need but too often they must see the first version before they can understand why I suspect it may not be what they need. Here I have broken my own rule and have given you what I think you need. If you really do need an even distribution, this code can easily be adapted to create it but I want you to see a “realistic” distribution first.
I have placed comments within my code but the finer points of the algorithm may not be clear. Try the code on a selection of redistribution problems. If it looks about right I can give further explanations and detail parts of the algorithm that may require fine tuning.
I have not removed my diagnostic code.
Option Explicit
Sub Control()
' For each worksheet
' * Validate and load maximum column value and matrix.
' * If maximum column value or matrix are faulty, output a message
' to below the matrix.
' * Call the redistribution algorithm.
' * Store result below the original matrix.
Dim Addr As String
Dim ColCrnt As Long
Dim ColMatrixLast As Long
Dim ErrMsg As String
Dim Matrix() As Long
Dim MatrixMaxColTotal As Long
Dim Pos As Long
Dim RowCrnt As Long
Dim RowMatrixLast As Long
Dim RowMsg As Long
Dim TotalMatrix As Long
Dim WSht As Worksheet
For Each WSht In Worksheets
ErrMsg = ""
With WSht
' Load MaxCol
If IsNumeric(.Cells(1, 1).Value) Then
MatrixMaxColTotal = Int(.Cells(1, 1).Value) ' Ignore any decimal digits
If MatrixMaxColTotal <= 0 Then
ErrMsg = "Maximum column value (Cell A1) is not positive"
End If
Else
ErrMsg = "Maximum column value (Cell A1) is not numeric"
End If
If ErrMsg = "" Then
' Find dimensions of matrix
If IsEmpty(.Cells(2, 2).Value) Then
ErrMsg = "Top left cell of matrix (Cell B2) is empty"
Else
Debug.Print .Name
If Not IsEmpty(.Cells(2, 3).Value) Then
' Position to last non-blank cell in row 2 after B2
ColMatrixLast = .Cells(2, 2).End(xlToRight).Column
Else
' Cell C2 is blank
ColMatrixLast = 2
End If
'Debug.Print ColMatrixLast
If Not IsEmpty(.Cells(3, 2).Value) Then
' Position to last non-blank cell in column 2 after B2
RowMatrixLast = .Cells(2, 2).End(xlDown).Row
Else
' Cell B3 is blank
RowMatrixLast = 2
End If
'Debug.Print RowMatrixLast
If ColMatrixLast = 2 Then
ErrMsg = "Matrix must have at least two columns"
End If
End If
End If
If ErrMsg = "" Then
' Load matrix and validation as all numeric
ReDim Matrix(1 To ColMatrixLast - 1, 1 To RowMatrixLast - 1)
TotalMatrix = 0
For RowCrnt = 2 To RowMatrixLast
For ColCrnt = 2 To ColMatrixLast
If Not IsEmpty(.Cells(RowCrnt, ColCrnt).Value) And _
IsNumeric(.Cells(RowCrnt, ColCrnt).Value) Then
Matrix(ColCrnt - 1, RowCrnt - 1) = .Cells(RowCrnt, ColCrnt).Value
TotalMatrix = TotalMatrix + Matrix(ColCrnt - 1, RowCrnt - 1)
Else
ErrMsg = "Cell " & Replace(.Cells(RowCrnt, ColCrnt).Address, "$", "") & _
" is not numeric"
Exit For
End If
Next
Next
If TotalMatrix > MatrixMaxColTotal * UBound(Matrix, 1) Then
ErrMsg = "Matrix total (" & TotalMatrix & ") > Maximum column total x " & _
"Number of columns (" & MatrixMaxColTotal * UBound(Matrix, 1) & ")"
End If
End If
RowMsg = .Cells(Rows.Count, "B").End(xlUp).Row + 2
If ErrMsg = "" Then
Call Redistribute(MatrixMaxColTotal, Matrix)
' Save answer
For RowCrnt = 2 To RowMatrixLast
For ColCrnt = 2 To ColMatrixLast
.Cells(RowCrnt + RowMsg, ColCrnt).Value = Matrix(ColCrnt - 1, RowCrnt - 1)
Next
Next
Else
.Cells(RowMsg, "B").Value = "Error: " & ErrMsg
End If
End With
Next
End Sub
Sub Redistribute(MaxColTotal As Long, Matrix() As Long)
' * Matrix is a two dimensional array. A row specifies the time
' spent on a single task. A column specifies the time spend
' during a single time period. The nature of the tasks and the
' time periods is not known to this routine.
' * This routine uses rows 1 to N and columns 1 to M. Row 0 and
' Column 0 could be used for headings such as task or period
' name without effecting this routine.
' * The time spent during each time period should not exceed
' MaxColTotal. The routine redistributes time so this is true.
Dim FixedCol() As Boolean
Dim InxColCrnt As Long
Dim InxColMaxTotal As Long
Dim InxColTgtLeft As Long
Dim InxColTgtRight As Long
Dim InxRowCrnt As Long
Dim InxRowSorted As Long
Dim InxTotalRowSorted() As Long
Dim Lng As Long
Dim TotalCol() As Long
Dim TotalColCrnt As Long
Dim TotalMatrix As Long
Dim TotalRow() As Long
Dim TotalRowCrnt As Long
Dim TotalRowRedistribute() As Long
Call DsplMatrix(Matrix)
ReDim TotalCol(1 To UBound(Matrix, 1))
ReDim FixedCol(1 To UBound(TotalCol))
ReDim TotalRow(1 To UBound(Matrix, 2))
ReDim InxTotalRowSorted(1 To UBound(TotalRow))
ReDim TotalRowRedistribute(1 To UBound(TotalRow))
' Calculate totals per column and set all entries in FixedCol to False
For InxColCrnt = 1 To UBound(Matrix, 1)
TotalColCrnt = 0
For InxRowCrnt = 1 To UBound(Matrix, 2)
TotalColCrnt = TotalColCrnt + Matrix(InxColCrnt, InxRowCrnt)
Next
TotalCol(InxColCrnt) = TotalColCrnt
FixedCol(InxColCrnt) = False
Next
' Calculate totals per row
For InxRowCrnt = 1 To UBound(Matrix, 2)
TotalRowCrnt = 0
For InxColCrnt = 1 To UBound(Matrix, 1)
TotalRowCrnt = TotalRowCrnt + Matrix(InxColCrnt, InxRowCrnt)
Next
TotalRow(InxRowCrnt) = TotalRowCrnt
Next
' Created sorted index into totals per row
' This sorted index allows rows to be processed in the total sequence
For InxRowCrnt = 1 To UBound(TotalRow)
InxTotalRowSorted(InxRowCrnt) = InxRowCrnt
Next
InxRowCrnt = 1
Do While InxRowCrnt < UBound(TotalRow)
If TotalRow(InxTotalRowSorted(InxRowCrnt)) > _
TotalRow(InxTotalRowSorted(InxRowCrnt + 1)) Then
Lng = InxTotalRowSorted(InxRowCrnt)
InxTotalRowSorted(InxRowCrnt) = InxTotalRowSorted(InxRowCrnt + 1)
InxTotalRowSorted(InxRowCrnt + 1) = Lng
If InxRowCrnt > 1 Then
InxRowCrnt = InxRowCrnt - 1
Else
InxRowCrnt = InxRowCrnt + 1
End If
Else
InxRowCrnt = InxRowCrnt + 1
End If
Loop
'For InxColCrnt = 1 To UBound(Matrix, 1)
' Debug.Print Right(" " & TotalCol(InxColCrnt), 3) & " ";
'Next
'Debug.Print
'Debug.Print
For InxRowCrnt = 1 To UBound(TotalRow)
Debug.Print Right(" " & TotalRow(InxRowCrnt), 3) & " ";
Next
Debug.Print
For InxRowCrnt = 1 To UBound(TotalRow)
Debug.Print Right(" " & InxTotalRowSorted(InxRowCrnt), 3) & " ";
Next
Debug.Print
Do While True
' Find column with highest total
InxColMaxTotal = 1
TotalColCrnt = TotalCol(InxColMaxTotal)
For InxColCrnt = 2 To UBound(TotalCol)
If TotalColCrnt < TotalCol(InxColCrnt) Then
TotalColCrnt = TotalCol(InxColCrnt)
InxColMaxTotal = InxColCrnt
End If
Next
If TotalColCrnt <= MaxColTotal Then
' Problem solved
Exit Sub
End If
' Find column to left, if any, to which
' surplus can be transferred
InxColTgtLeft = 0
For InxColCrnt = InxColMaxTotal - 1 To 1 Step -1
If Not FixedCol(InxColCrnt) Then
InxColTgtLeft = InxColCrnt
Exit For
End If
Next
' Find column to right, if any, to which
' surplus can be transferred
InxColTgtRight = 0
For InxColCrnt = InxColMaxTotal + 1 To UBound(TotalCol)
If Not FixedCol(InxColCrnt) Then
InxColTgtRight = InxColCrnt
Exit For
End If
Next
If InxColTgtLeft = 0 And InxColTgtRight = 0 Then
' Problem unsolvable
Call MsgBox("Redistribution impossible", vbCritical)
Exit Sub
End If
If InxColTgtLeft = 0 Then
' There is no column to the left to which surplus can be
' redistributed. Give its share to column on the right.
InxColTgtLeft = InxColTgtRight
End If
If InxColTgtRight = 0 Then
' There is no column to the right to which surplus can be
' redistributed. Give its share to column on the left.
InxColTgtRight = InxColTgtLeft
End If
'Debug.Print InxColTgtLeft & " " & InxColMaxTotal & " " & InxColTgtRight
' Calculate new value for each row of the column with maximum total,
' Calculate the value to be redistributed and the new column total
TotalColCrnt = TotalCol(InxColMaxTotal)
For InxRowCrnt = 1 To UBound(TotalRow)
Lng = Round(Matrix(InxColMaxTotal, InxRowCrnt) * MaxColTotal / TotalColCrnt, 0)
TotalRowRedistribute(InxRowCrnt) = Matrix(InxColMaxTotal, InxRowCrnt) - Lng
Matrix(InxColMaxTotal, InxRowCrnt) = Lng
TotalCol(InxColMaxTotal) = TotalCol(InxColMaxTotal) - TotalRowRedistribute(InxRowCrnt)
Next
If TotalCol(InxColMaxTotal) > MaxColTotal Then
' The column has not be reduced by enough.
' subtract 1 from the value for rows with the smallest totals until
' the column total has been reduced to MaxColTotal
For InxRowCrnt = 1 To UBound(TotalRow)
InxRowSorted = InxTotalRowSorted(InxRowCrnt)
Matrix(InxColMaxTotal, InxRowCrnt) = Matrix(InxColMaxTotal, InxRowCrnt) - 1
TotalRowRedistribute(InxRowCrnt) = TotalRowRedistribute(InxRowCrnt) + 1
TotalCol(InxColMaxTotal) = TotalCol(InxColMaxTotal) - 1
If TotalCol(InxColMaxTotal) = MaxColTotal Then
Exit For
End If
Next
ElseIf TotalCol(InxColMaxTotal) < MaxColTotal Then
' The column has be reduced by too much.
' Add 1 to the value for rows with the largest totals until
For InxRowCrnt = 1 To UBound(TotalRow)
InxRowSorted = InxTotalRowSorted(InxRowCrnt)
Matrix(InxColMaxTotal, InxRowCrnt) = Matrix(InxColMaxTotal, InxRowCrnt) + 1
TotalRowRedistribute(InxRowCrnt) = TotalRowRedistribute(InxRowCrnt) - 1
TotalCol(InxColMaxTotal) = TotalCol(InxColMaxTotal) + 1
If TotalCol(InxColMaxTotal) = MaxColTotal Then
Exit For
End If
Next
End If
' The column which did have the hightest total has now beed fixed
FixedCol(InxColMaxTotal) = True
' The values in TotalRowRedistribute must but added to the columns
' identified by InxColTgtLeft and InxColTgtRight
For InxRowCrnt = 1 To UBound(TotalRow)
Lng = TotalRowRedistribute(InxRowCrnt) / 2
Matrix(InxColTgtLeft, InxRowCrnt) = Matrix(InxColTgtLeft, InxRowCrnt) + Lng
TotalCol(InxColTgtLeft) = TotalCol(InxColTgtLeft) + Lng
Lng = TotalRowRedistribute(InxRowCrnt) - Lng
Matrix(InxColTgtRight, InxRowCrnt) = Matrix(InxColTgtRight, InxRowCrnt) + Lng
TotalCol(InxColTgtRight) = TotalCol(InxColTgtRight) + Lng
Next
Call DsplMatrix(Matrix)
Loop
End Sub
Sub DsplMatrix(Matrix() As Long)
Dim InxColCrnt As Long
Dim InxRowCrnt As Long
Dim TotalColCrnt As Long
Dim TotalMatrix As Long
Dim TotalRowCrnt As Long
For InxRowCrnt = 1 To UBound(Matrix, 2)
TotalRowCrnt = 0
For InxColCrnt = 1 To UBound(Matrix, 1)
Debug.Print Right(" " & Matrix(InxColCrnt, InxRowCrnt), 3) & " ";
TotalRowCrnt = TotalRowCrnt + Matrix(InxColCrnt, InxRowCrnt)
Next
Debug.Print " | " & Right(" " & TotalRowCrnt, 3)
Next
For InxColCrnt = 1 To UBound(Matrix, 1)
Debug.Print "--- ";
Next
Debug.Print " | ---"
TotalMatrix = 0
For InxColCrnt = 1 To UBound(Matrix, 1)
TotalColCrnt = 0
For InxRowCrnt = 1 To UBound(Matrix, 2)
TotalColCrnt = TotalColCrnt + Matrix(InxColCrnt, InxRowCrnt)
Next
Debug.Print Right(" " & TotalColCrnt, 3) & " ";
TotalMatrix = TotalMatrix + TotalColCrnt
Next
Debug.Print " | " & Right(" " & TotalMatrix, 3)
Debug.Print
End Sub