Excel VBA - Go through range and copy each cell 9 times - vba

I have a spreadsheet with data as follows:
G H I J K L M N O P Q R S T U V W X Y Z AA AB AC AD ... etc.
2 1
3 2
4 4 8 12 16 20 24 28 32 36 40
5 8 16 24 32 40
That is, G2 = 1, G3 = 1 ... M4 = 28 and so on...
What I need help with is going through this range, which can be dynamic as people are entering data into this range when they need to change stuff. I need to iterate through the rows and then columns and, for each cell that has a value, I need to paste it into a different sheet in column D, 9 times for each cell.
That is, on the 2nd sheet, the data above would come across as:
Column
D
1
1
1
1
1
1
1
1
1
2
2
2
2
2
2
2
2
2
4
4
.. etc...
How do I iterate through each row, and then each column and then for each cell that has a value, copy that 9 times into column D on another sheet, and then for the next cell with a value, copy that BELOW what was pasted and so on?

Try the following. It assumes you want to go column by column iterating through all the populated cells in that column, repeating the value 9 times.
Option Explicit
Public Sub OutputRepeatedValues()
Dim arr()
Const DELIMITER As String = ","
Const NUMOFTIMES As Long = 9
With ThisWorkbook.Worksheets("Sheet1")
arr = .Range(.Range("G2"), .Range("G2").SpecialCells(xlLastCell)).Value
End With
Dim i As Long, j As Long, output As String
For i = LBound(arr, 2) To UBound(arr, 2) '<== iterate rows with a column, column by column
For j = LBound(arr, 1) To UBound(arr, 1)
If Not IsEmpty(arr(j, i)) Then output = output & Replicate(arr(j, i), NUMOFTIMES, DELIMITER)
Next j
Next i
output = Left$(output, Len(output) - 1)
ThisWorkbook.Worksheets("Sheet2").Range("B1").Resize(Len(output), 1) = Application.WorksheetFunction.Transpose(Split(output, DELIMITER))
End Sub
'Adapted from #this https://codereview.stackexchange.com/questions/159080/string-repeat-function-in-vba?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
Public Function Replicate(ByVal RepeatString As String, ByVal NUMOFTIMES As Long, Optional ByVal DELIMITER As String = ",")
Dim s As String, c As Long, l As Long, i As Long
l = Len(RepeatString) + 1
c = l * NUMOFTIMES
s = Space$(c)
For i = 1 To c Step l
Mid(s, i, l) = RepeatString & DELIMITER
Next i
Replicate = s
End Function
Notes:
Test dataset laid out as shown below
I assume that you want to work with what ever data is down or the right of G2, including G2. In order to do this I am using SpecialCells(xlLastCell) to find the last used cell. I then construct a range with .Range(.Range("G2"), .Range("G2").SpecialCells(xlLastCell)), which in this case is $G$2:$Q$5, and read that into an array.
Assume that you indeed iterate rows with a column before moving onto next column as described in your question. I concatenate the populated cells values whilst at the same time calling the Replicate function described in 4).
I have hijacked, and adapted, a performant function by #this, to handle the string repeat. I have added in an optional argument for delimiter. A delimiter is added so I can later split on this to write out the results to individual cells within the target worksheet.
I split the string, output, on the delimiter, this creates an array of the repeated values, which I transpose, so I can write out to a column in the target sheet.
Example output:
Edit:
If instead you want to loop the rows, then columns, use with the above function the following instead:
Public Sub OutputRepeatedValues()
Dim arr()
Const DELIMITER As String = ","
Const NUMOFTIMES As Long = 9
With ThisWorkbook.Worksheets("Sheet1")
arr = .Range(.Range("G2"), .Range("G2").SpecialCells(xlLastCell)).Value
End With
Dim i As Long, j As Long, output As String
For i = LBound(arr, 1) To UBound(arr, 1) '<== iterate rows with a column, column by column
For j = LBound(arr, 2) To UBound(arr, 2)
If Not IsEmpty(arr(i, j)) Then output = output & Replicate(arr(i, j), NUMOFTIMES, DELIMITER)
Next j
Next i
output = Left$(output, Len(output) - 1)
ThisWorkbook.Worksheets("Sheet2").Range("B1").Resize(Len(output), 1) = Application.WorksheetFunction.Transpose(Split(output, DELIMITER))
End Sub

My vba is rusty but I think this (pseudo) code might help you.
def last_row as integer, last_col as integer, row as integer, col as integer, target as integer
'I like something like this to get the value but you have to know the largest column: Cells(Rows.Count, col_to_check).End(xlUp).Row
target = 1
for col = 7 to last_col '7 = G
for row = 2 to last_row
if(Not IsEmpty(Cells(row,col)) then
Range(Cells(target*9-8, 4), Cells(target*9, 4))= Cells(row,col)
target = target +1
end
next row
next col
this iterates through all cols and rows checks if there is a value and the copies it to a 9-cell range then iterates the target so it will point to the next 9 cells.

Related

Find Existing String in 2-D Array

I'm collecting data from a spreadsheet and storing it in a 2-D Array, the idea is that once the script detects it's reading from a specific column, it would not read an entire row of the data (as this would be considered a duplicate).
CODE:
Private Sub LoadData()
cDOC_DEBUG "Loading document data..."
Dim x As Long 'Column Data - there is another function that reads when x = 0 = header; else every other value is considered "data"
Dim y As Long
With dataWS
For x = 1 To LR - 1
For y = 1 To LC - 1
If (IsInArray(.Cells(x + 1, y + 1).value, pData())) Then
cDOC_DEBUG "Added: " & .Cells(x + 1, y + 1).value
pData(x, y) = Trim(.Cells(x + 1, y + 1).value)
End If
Next y
Next x
End With
End Sub
Private Function IsInArray(stringToBeFound As String, arrString As Variant) As Boolean
IsInArray = (UBound(Filter(arrString, stringToBeFound)) > -1)
End Function
Private Sub cDOC_DEBUG(debugText As String)
If (ThisWorkbook.Worksheets("Settings").Cells(3, 2)) Then
Debug.Print debugText
End If
End Sub
Everything is loading into the array fine, until I start implementing my IsInArray function. I can see it has to do with the fact that it's searching through a single dimensional array, and my array is two dimensional; so it makes sense that it's getting a type mismatch error.
Each row within the spreadsheet is a segment of information that correlates to it's self.
Initial Data From Spreadsheet:
A B C D
1 header1 header2 header3 header4
2 a b c d
3 w x y z
4 a h j j
5 a b j d
6 w x u z
2x2 Final Array:
0 1 2 3
0 header1 header2 header3 header4
1 a b c d
2 w x y z
3 a h j j
Because Header1 & Header2 & Header4 from Excel rows 5 & 6 have the same values as Excel rows 2 and 3, this will not be read into the array.
Question:
How would I match the criteria above to not include the duplicates from a row.
Example Sudo Code:
If (Value being added matches all values from column Header1 & Header2 & Header3_ Then
Don't add to array
Another issue that I am aware of, is that there will be blank data within this array; is there something I can do to either 1 remove these or will I have to have another index for the array slots to keep track of?
You can loop rows/columns and use Index to slice a row/column out of the array and use Match to test if search value is in that column. Combine with Count to test for duplicates. If the count equals the number of columns ignore value (or column count -1... see next comment ==>). Not entirely sure about this imaginary column. Do you intend to dimension at start with an additional empty column?
Row Versions:
Exists:
Option Explicit
Public Sub CheckRow()
Dim arr(), i As Long
arr = [A1:D6].Value '<==2D array created
For i = LBound(arr, 1) To UBound(arr, 1) '<== loop rows
'look in each row for x and if found exit loop and indicate row where found
If Not IsError(Application.Match("x", Application.WorksheetFunction.Index(arr, i, 0), 0)) Then
Debug.Print "value found in column " & i
Exit For
End If
Next
End Sub
Duplicates:
Option Explicit
Public Sub CheckRow()
Dim arr(), i As Long
arr = [A1:D6].Value '<==2D array created
For i = LBound(arr, 1) To UBound(arr, 1) '<== loop rows
'look in each row for more than one "B" and if found exit loop and indicate row where found
If Application.Count(Application.Match(Application.WorksheetFunction.Index(arr, i, 0), "B", 0)) > 1 Then
Debug.Print i
Exit For
End If
Next
End Sub
exists:
Columns versions:
Exists:
Option Explicit
Public Sub CheckColumn()
Dim arr(), i As Long
arr = [A1:D6].Value '<==2D array created
For i = LBound(arr, 2) To UBound(arr, 2) '<== loop columns
'look in each column for x and if found exit loop and indicate column where found
If Not IsError(Application.Match("x", Application.WorksheetFunction.Transpose(Application.WorksheetFunction.Index(arr, 0, i)), 0)) Then
Debug.Print "value found in column " & i
Exit For
End If
Next
End Sub
Duplicates:
You can use Count to check for duplicates within an entire column, again sliced with Index:
Option Explicit
Public Sub CheckColumn()
Dim arr(), i As Long
arr = [A1:D6].Value '<==2D array created
For i = LBound(arr, 2) To UBound(arr, 2) '<== loop columns
'look in each column for more than one "B" and if found exit loop and indicate column where found
If Application.Count(Application.Match(Application.WorksheetFunction.Transpose(Application.WorksheetFunction.Index(arr, 0, i)), "B", 0)) > 1 Then
Debug.Print i
Exit For
End If
Next
End Sub
Using sample data in sheet:
Alternative using advanced Index function
This approach using a (late bound) dictionary should be helpful, if your data rows don't exceed the number of 65536. You'll get a 2-dim (1-based) array v with the unique data set of columns A,B and D.
In this example code results are written back to e.g. columns F:H and values of column C are omitted; if you want to maintain these values see ► Edit below.
Example code (omitting column C in resulting array)
Sub getUniqueRows()
Dim dict As Object, v, i&, ii&, n&, currRow$
Set dict = CreateObject("Scripting.Dictionary") ' late binding dictionary
With ThisWorkbook.Worksheets("MySheet") ' << change to your sheet name
n = .Cells(.Rows.Count, "A").End(xlUp).Row - 1 ' n items (omitting header line)
' [1] get data
v = .Range("A2:D" & n + 1).Value
' [2a]remove column C (i.e. allow columns 1, 2 and 4 only)
v = Application.Index(v, Evaluate("row(1:" & n & ")"), Array(1, 2, 4))
' [2b] check for unique ones
For i = 1 To n
currRow = Join(Application.Index(v, i, 0), ",") ' build string of cells A,B & D
If Not dict.Exists(currRow) Then dict.Add currRow, i
Next i
' [3] remove duplicate rows
v = Application.Transpose(Application.Index(v, dict.Items, Evaluate("row(1:" & 3 & ")")))
' [4] write data to any wanted range
.Range("F:H") = "" ' clear rows
.Range("F2").Resize(UBound(v), 3) = v ' write data
End With
Set dict = Nothing
End Sub
Note
The dict.Items collection in section [3] is an array of all found item numbers in the dictionary and allows the Index function to get only these items.
Additional links
See Insert new first column in datafield array without loops or API call
Edit - maintain values in column C
Due to comment: "ONLY using columns A, B, and D; Column C was not including in the criteria."
If you want to check values only in A,B and D, but maintain the C values in the resulting array you can use the following optimized code neglecting an empty values row.
Sub getUniqueRows2()
Dim dict As Object, v, i&, n&, j&, currRow$
Set dict = CreateObject("Scripting.Dictionary") ' late binding dictionary
With ThisWorkbook.Worksheets("MySheet") ' << change to your sheet name
n = .Cells(.Rows.Count, "A").End(xlUp).Row - 1 ' items counter (omitting header line)
' [1] get data
v = .Range("A2:D" & n + 1).Value
' [2] check for unique ones
For i = 1 To UBound(v)
' assign ONLY criteria of 1st, 2nd & 4th column to string value currRow
currRow = ""
For j = 0 To 2: currRow = currRow & v(i, Array(1, 2, 4)(j)) & ",": Next j
' add first unique occurrence to dictionary
If Not dict.Exists(currRow) Then ' add first occurrence
If Len(currRow) > 3 Then dict.Add currRow, i ' ... and ignore empty values
End If
Next i
' [3] remove duplicate rows
v = Application.Transpose(Application.Index(v, dict.Items, Evaluate("row(1:" & 4 & ")")))
' [4] write resulting array values anywhere, e.g. to columns F:I
.Range("F:I") = "" ' clear rows
.Range("F2").Resize(UBound(v), 4) = v ' write data
End With
Set dict = Nothing
End Sub

VBA - Breaking a cell string into individual cells based on length

I have a cell with a string of different lengths. I want split them into individual cells with a length of, say, 3 characters.
A cell with ABCCBA should end up ABC CBA in 2 different cells.
While a cell with ABCDABCDAB should end up ABC DAB CDA B in 4 different cells.
Is there any convenient way to do this?
I was looking at
' Finding number of cells
Segments = WorksheetFunction.RoundUp(Len(Range("A1").Value) / 3, 0)
' Split base on character length
For n = 1 to Segments
Cells(2, n) = Range("A1").Characters(n, 3)
Next n
But it doesn't seem to work.
A simple macro to split the string in to 3 lettered strings and write into columns next to the data range
Sub Split()
Dim Checkcol As Integer
Dim currentRowValue As String
Dim rowCount As Integer
Dim splitval As Integer
Dim i As Integer, j As Integer
Checkcol = 1 'Denotes A column
rowCount = Cells(Rows.Count, Checkcol).End(xlUp).Row
For currentRow = 1 To rowCount
currentRowValue = Cells(currentRow, Checkcol).Value
splitval = Int(Len(currentRowValue) / 3) + 1 'Find the number of 3 letter strings
j = 0
For i = 1 To splitval 'Loop through each value and write in next columns
j = (i - 1) * 3 + 1
Cells(currentRow, Checkcol + i).Value = Mid(currentRowValue, j, 3)
Next
Next
End Sub
If you are comfortable with formulas, assuming your data is in Cell A2, and you want to implement the formula in Cell B2 and rightwards.
Formula in B2:
=MID($A2,(COLUMNS($B$2:B2)-1)*3+1,3)
Copy it down and across as much you need.

Column header for different sheet

I have many rows having data in sheet 2 and I want the column name of the max of a row (i.e. from column name of B2 to AH2 inside if loop).
Sub shanaya()
Dim j As Integer
Dim i As Integer
Dim z As Integer
Dim x As Integer
z = 35
For i = 11 To 28
For j = 2 To 19
If Sheet8.Cells(j, 1) = Sheet1.Cells(i, 1) Then
Sheet1.Cells(i, 10) = Sheet8.Cells(j, z)
Max [(Sheet8.Cells(J,2)): (Sheet8.Cells(j,z))]
Sheet1.Cells(i,13) = column header of max function
End If
Next j
Next i
End Sub
The two key lines you need are along the lines of:
MaxVal = application.max(sheet8.range(sheet8.cells(j,2),sheet8.cells(j,z)))
sheet1.cells(i,13) = sheet8.cells(1,application.match(MaxVal,sheet8.rows(j)))
The first line finds the maximum value in the row. The second line returns the column header (presumably in row 1 as you're searching from row 2).

Listing all the numbers within a range where range is specified in two cells - vba

I have an excel sheet with the following data:
col1 col2 col3 col4
dvdtable 6 52 57
tvunit 2 30 31
I need to copy each row in another sheet, however making 6 copies of the dvdtable row and 2 copies of the tvunit row. (col2 is referring to the quantity). In addition I need to create a new column where for each of the 6 dvdtable rows I include 52,53,54,55,56,57 respectively in the new column. See the result below:
col1 col2 col3
dvdtable 6 52
dvdtable 6 53
dvdtable 6 54
dvdtable 6 55
dvdtable 6 56
dvdtable 6 57
tvunit 2 30
tvunit 2 31
I managed to produce the code that makes multiple copies of rows thanks to another question in your forum, but I am stuck with the last part of the programming, where I need to create the list of numbers within the range given in column 3 and column 4 for each type of furniture.
You likely have to change the sheetnames.
Option Explicit
Sub whyDidIDoThisForYou()
Dim i, j, k As Integer
Dim numbRows As Integer
Dim curWriteRow As Integer
Dim temp As Integer
Dim values() As String
numbRows = Range("a1").End(xlDown).Row - 1 'assumes heading
curWriteRow = 1
ReDim values(1 To numbRows, 1 To 4)
For i = 1 To numbRows
'read all values in from initial datasheet
For j = 1 To 4
values(numbRows, j) = Sheets("Sheet1").Cells(i + 1, j).Value
Next j
'write to next sheet
'get number of things to write
temp = values(numbRows, 4) - values(numbRows, 3)
'start writing the "output" sheet!
For j = 0 To temp
Sheets("Sheet2").Cells(curWriteRow, 1).Value = values(numbRows, 1)
Sheets("Sheet2").Cells(curWriteRow, 2).Value = values(numbRows, 2)
Sheets("Sheet2").Cells(curWriteRow, 3).Value = values(numbRows, 3) + j
curWriteRow = curWriteRow + 1
Next j
Next i
End Sub
You could use arrays as below which is much quicker than writing to ranges cell by cell
The code below
reads the orginal data into a variant array Y
loops through each row of Y (lngCnt2)
runs through that Y by the number of times specifiec in colulmB (lngCnt3)
dumps the new records to a second variant array X
dumps x to a range starting in E1 when finished
Sub SplicenDice()
Dim rng1 As Range
Dim lngCnt As Long
Dim lngCnt2 As Long
Dim lngCnt3 As Long
Dim lngCnt4 As Long
Dim X
Dim Y
Set rng1 = Range([a1], Cells(Rows.Count, "D").End(xlUp))
Y = rng1.Value2
lngCnt = Application.WorksheetFunction.Sum(Range("B:B"))
ReDim X(1 To lngCnt, 1 To 3)
For lngCnt2 = 1 To UBound(Y, 1)
For lngCnt3 = 1 To Y(lngCnt2, 2)
lngCnt4 = lngCnt4 + 1
X(lngCnt4, 1) = Y(lngCnt2, 1)
X(lngCnt4, 2) = Y(lngCnt2, 2)
X(lngCnt4, 3) = Y(lngCnt2, 3) + lngCnt3 - 1
Next
Next
[e1].Resize(UBound(X, 1), UBound(X, 2)).Value2 = X
End Sub

Excel Loop through list,transpose and create a matrix based on cell content

I am receiving a large file 500k+ lines but all the content is in column A. I need to run a macro that will transpose the data into matrix form but will only create a new row when it finds "KEY*" in the ActiveCell. For example:
| KEY 4759839 | asljhk | 35049 | | sklahksdjf|
| KEY 359 | skj | 487 |y| 2985789 |
The above data in my file would originally look like this in column A:
KEY 4759839
asljhk
35049
sklahksdjf
KEY 359
skj
487
y
2985789
Considerations:
Blank cells need to be transposed as well, so the macro cant stop based on emptyCell
The number of cells between KEY's is not constant so it actually needs to read the cell to know if it should create a new row
It can either stop based on say 20 empty cells in a row or prompt for a max row number
(Optional) It would be nice if there was some sort of visual indicator for the last item in a row so that its possible to tell if the last item(s) were blank cells
I searched around and found a macro that had the same general theme but it went based on every 6 lines and I did not know enough to try to modify it for my case. But in case it helps here it is:
Sub kTest()
Dim a, w(), i As Long, j As Long, c As Integer
a = Range([a1], [a500000].End(xlUp))
ReDim w(1 To UBound(a, 1), 1 To 6)
j = 1
For i = 1 To UBound(a, 1)
c = 1 + (i - 1) Mod 6: w(j, c) = a(i, 1)
If c = 6 Then j = j + 1
Next i
[c1].Resize(j, 6) = w
End Sub
I would greatly appreciate any help you can give me!
This works with the sample data you provided in your question - it outputs the result in a table starting in B1. It runs in less than one second for 500k rows on my machine.
Sub kTest()
Dim originalData As Variant
Dim result As Variant
Dim i As Long
Dim j As Long
Dim k As Long
Dim countKeys As Long
Dim countColumns As Long
Dim maxColumns As Long
originalData = Range([a1], [a500000].End(xlUp))
countKeys = 0
maxColumns = 0
'Calculate the number of lines and columns that will be required
For i = LBound(originalData, 1) To UBound(originalData, 1)
If Left(originalData(i, 1), 3) = "KEY" Then
countKeys = countKeys + 1
maxColumns = IIf(countColumns > maxColumns, countColumns, maxColumns)
countColumns = 1
Else
countColumns = countColumns + 1
End If
Next i
'Create the resulting array
ReDim result(1 To countKeys, 1 To maxColumns) As Variant
j = 0
k = 1
For i = LBound(originalData, 1) To UBound(originalData, 1)
If Left(originalData(i, 1), 3) = "KEY" Then
j = j + 1
k = 1
Else
k = k + 1
End If
result(j, k) = originalData(i, 1)
Next i
With ActiveSheet
.Cells(1, 2).Resize(UBound(result, 1), UBound(result, 2)) = result
End With
End Sub
Tested and works:
Sub test()
Row = 0
col = 1
'Find the last not empty cell by selecting the bottom cell and moving up
Max = Range("A650000").End(xlUp).Row 'Or whatever the last allowed row number is
'loop through the data
For i = 1 To Max
'Check if the left 3 characters of the cell are "KEY" and start a new row if they are
If (Left(Range("A" & i).Value, 3) = "KEY") Then
Row = Row + 1
col = 1
End If
Cells(Row, col).Value = Range("A" & i).Value
If (i > Row) Then
Range("A" & i).Value = ""
End If
col = col + 1
Next i
End Sub