Normalizing an Excel Table - excel

I have excel table exported from another system. I need to upload this into a database and am looking for a way to normalize it.
Current structure:
customerid date1 date2 date3 ... date85
1 1/1 1/4 2/4
2 3/1
3 4/1 4/12
Need to convert to:
customerid date
1 1/1
1 1/4
1 2/4
2 3/1
3 4/1
3 4/12
I'm on a Mac and have excel 2016. I don't have any addons like power pivot.

You can use the Pivot Table Wizard for this (which is not an add-on, it's built-in Excel functionality):
Press Alt, D, P to open the Pivot Table Wizard
Select "Multiple Consolodation Ranges", and click Next
Select "I will create the page fields", and click Next
With the cursor in the "Range" textbox, select your range of data, including row headers (in your example, I believe that would be A1:CH4), and click Add, then click Next
Select "New worksheet", and click Finish
In the new worksheet that is generated, double-click in the bottom-right cell (the "Grand Total" cell). This will open a second new worksheet containing your normalized data.
To remove blank values in the second new worksheet, click the filter icon in the "Value" column, and de-select the "(Blanks)" checkbox
Copy the resulting data to the clipboard, and paste wherever it is needed.

Assuming customerid is in A1, please try (I haven't!) in Row 2 in a column:
=OFFSET(A$2,INT((ROW()-2)/85),)
and in another column:
=OFFSET(B$2,INT(ROW()/85),MOD(ROW()-2,85),)
Copy down to suit, select the formulae cells, Copy, Paste Special..., Values over the top and then Filter to remove zeros in 'another' column.
Beware if doing this all in the one sheet as deleting rows might also delete some of your source data.
Add labels.

Here is a VBA method that should work quite rapidly, even on a large data base.
Note that you have to rename the class module as noted in that module.
Also note that you may have to rename wsSrc and wsRes -- the worksheets with your source data and where you want the results to go.
There is also an area near the end of the regular module where I do some rudimentary formatting. You can certainly adjust that to pretty things up, if you need to.
Class Module
Option Explicit
'Rename cCustDTS
Private pID As String
Private pDT As Date
Private pDTs As Collection
Private Sub Class_Initialize()
Set pDTs = New Collection
End Sub
Public Property Get ID() As String
ID = pID
End Property
Public Property Let ID(Value As String)
pID = Value
End Property
Public Property Get DT() As Date
DT = pDT
End Property
Public Property Let DT(Value As Date)
pDT = Value
End Property
Public Property Get DTs() As Collection
Set DTs = pDTs
End Property
Public Function ADDdt(Value As Date)
pDTs.Add Value
End Function
Regular Module
Option Explicit
Sub NormalizeDates()
Dim vSrc As Variant, vRes As Variant
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim cCD As cCustDTS, colCD As Collection
Dim I As Long, J As Long, LineCount As Long
Dim LastRow As Long, LastCol As Long
Dim V As Variant, W As Variant
'Set Source and Results Worksheets and Ranges
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 1)
'Get Source Data
With wsSrc
LastRow = .Cells(.Rows.Count, 1).End(xlUp).Row
LastCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
vSrc = .Range(.Cells(1, 1), .Cells(LastRow, LastCol))
End With
'Collect and organize the data
Set colCD = New Collection
For I = 2 To UBound(vSrc, 1) 'Skip the first row
Set cCD = New cCustDTS
With cCD
.ID = vSrc(I, 1)
For J = 2 To UBound(vSrc, 2)
If IsDate(vSrc(I, J)) Then
.DT = vSrc(I, J)
.ADDdt .DT
End If
Next J
colCD.Add cCD
LineCount = LineCount + .DTs.Count
End With
Next I
'Organize the data for output
ReDim vRes(0 To LineCount, 1 To 2)
vRes(0, 1) = "Customer ID"
vRes(0, 2) = "Date"
I = 0
For Each V In colCD
For Each W In V.DTs
I = I + 1
vRes(I, 1) = V.ID
vRes(I, 2) = W
Next W
Next V
'Write to the output sheet and format
Set rRes = rRes.Resize(rowsize:=UBound(vRes, 1) + 1, columnsize:=UBound(vRes, 2))
With rRes
.EntireColumn.Clear
.Value = vRes
With .Rows(1)
.Font.Bold = True
.HorizontalAlignment = xlCenter
End With
With .Columns(2)
.NumberFormat = "m/d"
End With
.EntireColumn.AutoFit
.Columns(2).ColumnWidth = .Columns(2).ColumnWidth * 2
End With
End Sub

Here's what I came up with, I hope VBA isn't that much different on Mac then it is on PC, otherwise this may not work.
The code is fairly commented, although feel free to ask if there are questions.
'Helper function to find the last Column
Public Function getLastColumn(strSheet, strColum) As Integer
Dim rng As Range
Set rng = Sheets(strSheet).Cells.Find(What:="*", _
After:=Sheets(strSheet).Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False)
If rng Is Nothing Then
getLastColumn = 1
Else
getLastColumn = rng.Column
End If
End Function
'Helper function to find the lastRow
Public Function getLastRow(strSheet, strColum) As Long
Dim rng As Range: Set rng = Worksheets(strSheet).Range(strColum & "1")
getLastRow = Worksheets(strSheet).Cells(Rows.Count, rng.Column).End(xlUp).row
End Function
Public Sub Normalize_Table()
Dim LastRow As Long: LastRow = getLastRow("Sheet1", "A") ' First Parameter is the Sheet Name,
' Second is the column you want to count
Dim LastColumn As Integer: LastColumn = getLastColumn("Sheet1", "A")
Dim RowCounter As Long: RowCounter = 1 ' Starting Row
Dim RowID As Variant ' RowID, basically this is the repeated Column 1 value
Dim row As Object
Dim col As Object
Dim rng As Range: Set rng = Sheets("Sheet1").Range(Sheets("Sheet1").Cells(1, 1), _
Sheets("Sheet1").Cells(LastRow, LastColumn)) ' Get the range you want to Normalize to another sheet/range
'Iterate the range, go through each row, and each column
'Making a new row for each column value, only update the value
'of the first column when a start a new row
For Each row In rng.Rows
RowID = rng.Cells(row.row, 1)
For Each col In rng.Columns
'Assuming you want to add this to a new sheet, Let's say "Sheet2"
Sheets("Sheet2").Cells(RowCounter, 1) = RowID
If col.Column > 1 Then
Sheets("Sheet2").Cells(RowCounter, 2) = rng(row.row, col.Column)
RowCounter = RowCounter + 1
End If
Next
Next
End Sub

Related

Excel VBA- VLOOKUP with multiple criteria

I need a VBA code that searches for a specific Name (first dropdown), Products (second drop-down), then returns the unit price. I can use VLOOKUP to search names and return the unit price but I need to search for names and products and be able to pull the prices quickly. I used Evaluate function but the result is #VALUE!
Sub unitPrice()
Set sh4 = ThisWorkbook.Sheets("Invoice")
Set sh5 = ThisWorkbook.Sheets("Unit Price")
sh4.Range("H18") = _
sh4.Evaluate("MATCH(" & sh4.Cells(11, 1).Address(False, False) _
& "&" & sh4.Cells(18, 1).Address(False, False) _
& ",'Sh5!B2:B5&sh5!A2:A5,0)")
End Sub
Screenshot of Invoice and Unit Price sheet
I am assuming that you have two tables (insert > table): tblInvoice and tblUnitPrice. It is much easier to reference them in VBA via listobject than without. If you are not using tables you have to adjust the ranges accordingly.
What my code does: It inserts an INDEX/MATCH-Formula to retrieve the Unitprice for all rows in the table - and then writes the pure values back to the cells.
Public Sub updateUnitPricesInInvoice()
Dim loInvoice As ListObject
Set loInvoice = ThisWorkbook.Worksheets("Invoice").ListObjects("tblInvoice")
With loInvoice.ListColumns("UnitPrice").DataBodyRange
.Formula2 = "=INDEX(tblUnitPrices[UnitPrice],MATCH(1,(tblUnitPrices[Name]=[#Name])*(tblUnitPrices[Product]=[#Product])))"
.value = .value
End With
End Sub
Alternative solution minimising interaction with sheet by matching in memory:
Option Explicit
Sub SimpleMatch()
Dim sh5 As Worksheet, sh4 As Worksheet 'declare vars
Set sh4 = ThisWorkbook.Sheets("Invoice") 'set sheet
Set sh5 = ThisWorkbook.Sheets("Unit Price") 'set sheet
Dim arr, arr2, LastRowSh4 As Long, LastRowSh5 As Long
LastRowSh4 = sh4.Cells(sh4.Rows.Count, "A").End(xlUp).Row 'count rows from last row
LastRowSh5 = sh5.Cells(sh5.Rows.Count, "A").End(xlUp).Row 'count rows from last row
arr = sh4.Range(sh4.Cells(1, 1), sh4.Cells(LastRowSh4, 8)).Value2 'load invoices to mem
arr2 = sh5.Range(sh5.Cells(1, 1), sh5.Cells(LastRowSh5, 3)).Value2 'load prices to mem
Dim j As Long, dict As Object
Set dict = CreateObject("Scripting.Dictionary") 'create dictionary lateB
With dict 'used because I'm to lazy to retype dict everywhere :)
.CompareMode = 1 'textcompare
For j = 1 To UBound(arr2) 'add prices to dict
If Not .Exists(arr2(j, 1) & arr2(j, 2)) Then 'set key if I don't have it yet in dict
.Add Key:=arr2(j, 1) & arr2(j, 2), Item:=arr2(j, 3)
End If
Next j
Dim cust As String
For j = 1 To UBound(arr)
If arr(j, 1) = "Bill To:" Then
cust = arr(j + 1, 1) 'assumes you have only 1 customer in the sheet!
End If
If .Exists(arr(j, 1) & cust) Then 'retrieve produc & cust price
arr(j, 8) = dict(arr(j, 1) & cust) 'add to arr
End If
Next j
End With
With sh4
.Range(.Cells(1, 1), .Cells(UBound(arr), UBound(arr, 2))) = arr 'dump updated array to invoice sheet
End With
End Sub
This is the solution without tables/listobjects:
Assumption: you have added names for the following cells on invoice sheet
A11: customer
A17: labelDescription
H17: labelUnitPrice
H28: labelTotalAmount
In the first step we retrieve the range between the two labels "UnitPrice" and "TotalAmount" - that's where the formula goes.
Then the formula is written to that range - using again INDEX/MATCH.
In case there is not description nothing is displayed (there ISERROR)
And again: after calculation formulas are replaced by their values
Option Explicit
Public Sub updateUnitPricesInInvoice()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Invoice")
Dim rgUnitPrices As Range
Set rgUnitPrices = getRangeBetweenTwoLabels(ws, "labelUnitPrice", "labelTotalAmount")
With rgUnitPrices
'Excel 365
'.Formula2 = "=IFERROR(INDEX(UnitPrice!C:C,MATCH(1,(UnitPrice!A:A=Invoice!" & ws.Range("labelDescription").Offset(1).Address(False, True) & ")*(UnitPrice!B:B=customer),0)),"""")"
'other Excel versions
With rgUnitPrices
.Formula = "=IFERROR(INDEX(UnitPrice!C:C,MATCH(1,(UnitPrice!A:A=Invoice!$A" & rgUnitPrices.Rows(1).Row & ")*(UnitPrice!B:B=customer),0)),"""")"
.FormulaArray = .FormulaR1C1
End With
.Value = .Value
End With
End Sub
Private Function getRangeBetweenTwoLabels(ws As Worksheet, _
label1 As String, label2 As String)
Dim cStart As Range: Set cStart = ws.Range(label1).Offset(1)
Dim cEnd As Range: Set cEnd = ws.Range(label2).Offset(-1)
Set getRangeBetweenTwoLabels = ws.Range(cStart, cEnd)
End Function

extract unique data

I got a sheet that contain weekly roster of each employee. The code below run perfectly to display unique data of one column:
Dim lastrow As Long
Application.ScreenUpdating = False
Dim rng, lastcell As Range
Set rng = Application.InputBox("Select a range", "Obtain Range Object", Type:=8)
lastrow = Cells(Rows.Count, "B").End(xlUp).Row
ActiveSheet.Range(rng.Address & lastrow).AdvancedFilter _
Action:=xlFilterCopy, _
CopyToRange:=ActiveSheet.Range(rng.Cells(rng.Rows.Count + 1, rng.Columns.Count).Address), _
Unique:=True
Application.ScreenUpdating = True
But my issue is that I want the code to exclude some text like OFF
and LEAVE. The only data to display is their shift which is in the format, 0430_1145 for timein_timeout in an asecending way.
The data normally is displayed at the end of each column:
If column have data such as:
0700_1500
0430_1145
leave
off
0700_1500
0830_1615
result would be(ascending way ignoring off and leave)-
0430_1145
0700_1500
0830_1615
Below is the link of my excel sheet:
https://drive.google.com/file/d/1CYGS9ZgsulG8J_qzYEUXWFiXkBHneibv/edit
If you have O365 with the appropriate functions, you can do this with a worksheet formula:
=SORT(UNIQUE(FILTER(A1:A6,(A1:A6<>"off")*(A1:A6<>"leave"))))
In the below image, the formula is entered into cell A8
Edit: Here is a VBA routine based on the worksheet you uploaded.
The result of the extraction of each column is stored as an ArrayList in a Dictionary.
I used an ArrayList because it is easy to sort -- but you could use any of a number of different objects to store this information, and write a separate sorting routine.
I also used late-binding for the dictionary and arraylist objects, but could switch that to early-binding if you have huge amounts of data to process and need the increased speed.
Note that the data is processed from a VBA array rather than on the worksheet.
many modifications are possible depending on your needs, but this should get you started.
Option Explicit
Sub summarizeShifts()
Dim wsSrc As Worksheet 'data sheet
Dim vSrc As Variant, vRes As Variant 'variant arrays for original data and results
Dim rRes As Range 'destination for results
Dim dShifts As Object ' store shifts for each day
Dim AL As Object 'store in AL to be able to sort
Dim I As Long, J As Long, S As String, V As Variant, W As Variant
'read source data into array
Set wsSrc = Worksheets("fnd_gfm_1292249")
With wsSrc
vSrc = .Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=9)
Set rRes = .Cells(UBound(vSrc, 1) + 1, 3) 'bottom of source data
End With
Set dShifts = CreateObject("Scripting.Dictionary")
'Populate the dictionary by columns
For J = 3 To UBound(vSrc, 2)
Set AL = CreateObject("System.Collections.ArrayList")
For I = 2 To UBound(vSrc, 1)
S = vSrc(I, J)
If S Like "####_####" Then
If Not AL.contains(S) Then AL.Add S
End If
Next I
AL.Sort
dShifts.Add J, AL
Next J
'size vres
I = 0
For Each V In dShifts
J = dShifts(V).Count
I = IIf(I > J, I, J)
Next V
ReDim vRes(1 To I, 1 To UBound(vSrc) - 2)
'populate results array
For Each V In dShifts
I = 0
For Each W In dShifts(V)
I = I + 1
vRes(I, V - 2) = W
Next W
Next V
'write the results
Application.ScreenUpdating = False
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.Resize(rowsize:=rRes.Rows.Count * 3).ClearContents 'or something to clear rows below the data
.Value = vRes
End With
End Sub
Approach via FilterXML()
In addition to the valid solutions above I demonstrate an alternative solution via FilterXML() available since vers. 2013+:
Sub ExtractUniques20201019()
'a) define Worksheet
Dim ws As Worksheet: Set ws = Sheet1 ' << change to project's sheet Code(Name)
'b) get first target Row (2 rows below original data)
Dim tgtRow As Long: tgtRow = UBound(getData(ws, "A", 1)) + 2
Dim i As Long
For i = 3 To 9 ' columns C:I (Monday to Sunday)
'[1] get data
Dim data: data = getData(ws, i) ' << function call getData()
'[2] get valid unique data
Dim uniques: uniques = getFilterUniques(data) ' << function call getFilterUniques()
BubbleSortColumnArray uniques ' << call procedure BubbleSortColumnArray
'[3] write results to target below data range
ws.Range("A" & tgtRow).Offset(columnoffset:=i - 1).Resize(UBound(uniques), 1) = uniques
Next i
End Sub
Help functions
Function getData(ws As Worksheet, ByVal col, Optional ByVal StartRow& = 2) As Variant()
' Purpose: assign column data to variant array
If IsNumeric(col) Then col = Split(ws.Cells(1, col).Address, "$")(1)
Dim lastRow As Long
lastRow = ws.Range(col & Rows.Count).End(xlUp).Row
getData = ws.Range(col & StartRow & ":" & col & lastRow).Value2
End Function
Function getFilterUniques(arr, Optional Fltr As String = "_")
'Purpose: get unique items containing e.g. Fltr "_" using XPath search
'Note: WorksheetFunction.FilterXML() is available since vers. 2013+
' XPath examples c.f. https://stackoverflow.com/questions/61837696/excel-extract-substrings-from-string-using-filterxml/61837697#61837697
Dim content As String ' well formed xml content string
content = "<t><s>" & Join(Application.Transpose(arr), "</s><s>") & "</s></t>"
getFilterUniques = WorksheetFunction.FilterXML(content, "//s[not(preceding::*=.)][contains(., '" & Fltr & "')]")
End Function
Bubblesort
Sub BubbleSortColumnArray(arr, Optional ByVal ColNo As Long = 1)
'Purpose: sort 1-based 2-dim datafield array
'correct differing column index
Dim colIdx As Long: colIdx = LBound(arr) + ColNo - 1
'bubble sort
Dim cnt As Long, nxt As Long, temp
For cnt = LBound(arr) To UBound(arr) - 1
For nxt = cnt + 1 To UBound(arr)
If arr(cnt, colIdx) > arr(nxt, colIdx) Then
temp = arr(cnt, colIdx) ' remember element
arr(cnt, colIdx) = arr(nxt, colIdx) ' swap
arr(nxt, colIdx) = temp
End If
Next nxt
Next cnt
End Sub
Consider using the one argument of AdvancedFilter you do not use: CriteriaRange. This can allow you to set up a multiple set criteria that leaves out those values. See Microsoft's Filter by using advanced criteria tutorial doc section: Multiple sets of criteria, one column in all sets.
Essentially, this involves adding a new region outside of data region somewhere in worksheet or workbook with column headers and needed criteria which would be <>LEAVE AND <>OFF which as link above shows would require two same named columns for AND logic.
Criteria Region
A B C D E F G H I J K L M N
1 Monday Monday Tuesday Tuesday Wednesday Wednesday Thursday Thursday Friday Friday Saturday Saturday Sunday Sunday
2 <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF <>LEAVE <>OFF
VBA
Below defines worksheet objects and avoids the use of ActiveSheet. See Two main reasons why .Select, .Activate, Selection, Activecell, Activesheet, Activeworkbook, etc. should be avoided.
...
Set data_ws = ThisWorkbook.Worksheets("myCurrentWorksheet")
Set criteria_ws = ThisWorkbook.Worksheets("myCurrentWorksheet")
data_ws.Range(rng.Address & lastrow).AdvancedFilter _
Action:=xlFilterCopy, _
CriteriaRange:=criteria_ws.Range("A1:N2")
CopyToRange:=data_ws.Range(rng.Cells(rng.Rows.Count + 1, rng.Columns.Count).Address), _
Unique:=True

VBA: Loop through merged cells and apply formatting for alternate rows

I've used VBA to filter out values from a different sheet and I'm thinking of how best to format it for readability.
I've merged similar values and would like to select the corresponding rows for each alternating merged cell and apply a color fill.
Here is a visual for reference:
And this is the code I've used to get to the current state.
Dim lRow As Long
lRow = Cells(Rows.Count, "B").End(xlUp).Row
Application.DisplayAlerts = False
For i = lRow To 7 Step -1
If Cells(i, 2) = Cells(i - 1, 2) Then
Range(Cells(i, 2), Cells(i - 1, 2)).Merge
End If
Next i
Application.DisplayAlerts = True
Is there a way of inserting formatting within the loop or otherwise? I'm also open to other ways of making the table more readable.
PS: The image I've attached is just for reference. The actual table I'm working with has tons of rows and columns so readability is important.
Except for the merging of cells the code below does what you want. Instead of merging the code effectively hides the duplicate item titles.
Option Explicit
Sub FormatData()
' 28 Feb 2019
Const CaptionRow As Long = 1
Const FirstDataRow As Long = 3 ' assuming row 2 to contain subtitles
Const FirstDataClm As String = "B" ' change as appropriate
Const DescClm As String = "D" ' change as appropriate
Dim Desc As Variant, PrevDesc As Variant
Dim Col() As Variant, ColIdx As Boolean
Dim FontCol As Long
Dim Rng As Range
Dim Rl As Long, Cl As Long ' last Row / Column
Dim R As Long
Rl = Cells(Rows.Count, DescClm).End(xlUp).Row
Cl = Cells(CaptionRow, Columns.Count).End(xlToLeft).Column
Col = Array(15261367, 15986394) ' sky, pale: change as required
FontCol = Cells(FirstDataRow, FirstDataClm).Font.Color
Application.ScreenUpdating = False
For R = FirstDataRow To Rl
Desc = Cells(R, DescClm).Value
If Desc = PrevDesc Then
Set Rng = Rng.Resize(Rng.Rows.Count + 1)
Else
If Not Rng Is Nothing Then
SetColouring Rng, DescClm, Col(Abs(ColIdx)), FontCol
ColIdx = Not ColIdx
End If
Set Rng = Range(Cells(R, FirstDataClm), Cells(R, Cl))
End If
PrevDesc = Desc
Next R
SetColouring Rng, DescClm, Col(Abs(ColIdx)), FontCol
Application.ScreenUpdating = True
End Sub
Private Sub SetColouring(Rng As Range, _
ByVal C As String, _
ByVal Col As Long, _
ByVal Fcol As Long)
' 28 Feb 2019
Dim R As Long
With Rng
.Interior.Color = Col
.Font.Color = Fcol
For R = 2 To .Rows.Count
.Cells(R, Columns(C).Column - .Column + 1).Font.Color = Col
Next R
End With
End Sub
There are some constants at the top of the code which you can modify. Note also that the font color you use in the sheet is presumed to be found in the first used cell of the sheet as specified by the constants.
Observe that the entire code runs on the ActiveSheet. I strongly urge you to change that bit and specify a sheet, preferably both by its name and the workbook it is in. If you regularly use the code as published above its just a matter of time before you apply it to a worksheet which gets damaged as a result.

How to get filtered data as rowsource of multicolumn listbox?

I have data in Sheet2 as like below.
Actual Data
Then I manually apply filer to those data which looks like...
Filtered Data
I have a user form (UserForm1) and a list box (ListBox1) in the form. Also have a command button cmdFilteredData. So, I want to fill the listbox with filtered data only. I make below codes but it gives Type mismatch error.
Private Sub cmdFilteredData_Click()
Dim FilteredRange As Range
Set FilteredRange = Sheet2.Range("A1:C5").Rows.SpecialCells(xlCellTypeVisible)
With Me.ListBox1
.ColumnCount = 3
.MultiSelect = fmMultiSelectExtended
.RowSource = FilteredRange
End With
End Sub
Any help is hearty appreciated.
Since you are trying to populate the ListBox1 with values from filtered range, you have blank rows in the middle, this "messes" up the ListBox.
Instead, you can copy>>Paste the value to columns on the right (or another worksheet), use an array to populate these values, and then populate the ListBox1 with the array.
Code
Private Sub cmdFilteredData_Click()
Dim FilteredRange As Range
Dim myArr As Variant
Set FilteredRange = ThisWorkbook.Sheets("Sheet8").Range("A1:C5").SpecialCells(xlCellTypeVisible)
' copy filtered range to the columns on the right (if you want, you can add a "Dummy" sheet), to have the range continous
FilteredRange.Copy Range("Z1")
' populae the array with new range values (without blank rows in the middle)
myArr = Range("Z1").CurrentRegion
With Me.ListBox1
.ColumnCount = 3
.MultiSelect = fmMultiSelectExtended
.List = (myArr)
End With
End Sub
Alternative Function to - unreliable - SpecialCells(xlCellTypeVisible)
This answer intends to complete Shai Rado's appreciated solution, not to correct it.
Testing the above solution, however showed that using SpecialCells(xlCellTypeVisible) and/or reference to CurrentRegion might result in problems (even within OP's small range).
A possible work around function (esp. for udfs) is presented at SpecialCells(xlCellTypeVisible) not working in UDF.
Private Function VisibleCells(rng As Range) As Range
' Site: https://stackoverflow.com/questions/43234354/specialcellsxlcelltypevisible-not-working-in-udf
' Note: as proposed by CalumDA
Dim r As Range
For Each r In rng
If r.EntireRow.Hidden = False Then
If VisibleCells Is Nothing Then
Set VisibleCells = r
Else
Set VisibleCells = Union(VisibleCells, r)
End If
End If
Next r
End Function
Shai Rado's solution slightly modified (cf. above notes)
In any case the target range has to be cleared before copying and then better referenced without CurrentRegion, so that you get the wanted items only. These changes worked for me.
Option Explicit
Private Sub cmdFilteredData_Click()
Dim ws As Worksheet
Dim sRng As String
Dim FilteredRange As Range
Dim myArr As Variant
Dim n As Long
Set ws = ThisWorkbook.Worksheets("Filtered")
n = ws.Range("A" & ws.Rows.Count).End(xlUp).Row ' get last row
sRng = "A1:C" & n
' Set FilteredRange = ws.Range(sRng).SpecialCells(xlCellTypeVisible) ' << not reliable
Set FilteredRange = VisibleCells(ws.Range(sRng)) ' <<<< possible ALTERNATIVE
' clear target range in order to allow correct array fillings later !
ws.Range("Z:AAB").Value = ""
' copy filtered range to the columns on the right
FilteredRange.Copy ws.Range("Z1")
' populate the array with new range values (without blank rows in the middle)
' myArr = ws.Range("Z1").CurrentRegion ' sometimes unreliable, too
myArr = ws.Range("Z1:AAB" & ws.Range("Z" & ws.Rows.Count).End(xlUp).Row) ' <<< better than CurrentRegion
With Me.ListBox1
.ColumnCount = 3
.MultiSelect = fmMultiSelectExtended
.List = (myArr)
End With
End Sub
Links mentioned in cited post:
Microsoft - udf not working
ExcelForum - xlCelltypeVisible not working
MrExcel - SpecialCells not working
I was searching a lot for that but I couldn't fine any elegant solution for doing it without pasting data in the sheet. So I create my own function to convert visible cells of range into an array.
Maybe it's not the smartest way, but works just fine an quite fast.
Function createArrFromRng(rng As Range)
Dim sCellValues() As Variant
Dim col, row, colCount, RowCount As Integer
col = 0
row = 0
colCount = 0
RowCount = 0
On Error GoTo theEnd
Set rng = rng.SpecialCells(xlCellTypeVisible)
'get the columns and rows size
For Each cell In rng
If col < cell.Column Then
colCount = colCount + 1
Else
colCount = 1
End If
col = cell.Column
If row < cell.row Then
RowCount = RowCount + 1
End If
row = cell.row
Next cell
'set the array size
ReDim Preserve sCellValues(RowCount - 1, colCount - 1)
col = 0
row = 0
colCount = 0
RowCount = 0
'get the values and add to the array
For Each cell In rng
If col < cell.Column Then
colCount = colCount + 1
Else
colCount = 1
End If
col = cell.Column
'Debug.Print colCount
If row < cell.row Then
RowCount = RowCount + 1
End If
row = cell.row
sCellValues(RowCount - 1, colCount - 1) = cell.value
Next cell
theEnd:
createArrFromRng = sCellValues
End Function

Normalizing Excel Grid Intersection data into a flat list

I am trying to get Excel data, which was mapped using a grid/matrix mapping into a de-normalized for so that i can enter the data into a database.
How do you copy data in a grid from one excel sheet to the other as follow illustrated below.
I was trying something like this... but as you can see, i am far off!
Sub NormaliseList(mySelection As Range)
Dim cell As Range
Dim i As Long
i = 1
For Each cell In mySelection
If cell <> "" Then
Sheets(2).Range("A" & i).Value = cell(cell.Row, 1).Value
Sheets(2).Range("B" & i).Value = cell.Value
Sheets(2).Range("C" & i).Value = cell(1, cell.Column).Value
i = i + 1
Next cell
End Sub
For Reference. I Updated my code..
Simply add the code, assign macro shortcut to the function
Select the range that contains the intersection data (not the row and column data)
Run macro (Beware, sheet 2 will have data added in normalised form)
If there are multiple headings that are needed i figured i would consolidate into one column then perform a "text to columns" after processing.
Sub NormaliseList()
' to run - assign macro shortcut to sub - Select Intersection data (not row and column headings and run)
Dim Rowname, ColumnName, IntValue As String
Dim x, cntr As Integer
Dim test As Boolean
cntr = 0
For x = 1 To Selection.Count
If Selection(x).Value <> "" Then
cntr = cntr + 1
Rowname = ActiveSheet.Cells(Selection.Cells(x).Row, Selection.Column - 1)
ColumnName = ActiveSheet.Cells(Selection.Row - 1, Selection.Cells(x).Column)
IntValue = Selection(x).Value
test = addrecord(Rowname, ColumnName, IntValue, cntr)
End If
Next x
End Sub
Function addrecord(vA, vB, vC As String, rec As Integer) As Boolean
'Make sure that you have a worksheet called "Sheet2"
Sheets("Sheet2").Cells(rec, 1) = vA
Sheets("Sheet2").Cells(rec, 2) = vB
Sheets("Sheet2").Cells(rec, 3) = vC
End Function
I've got two posts, with usable code and downloadable workbook, on doing this in Excel/VBA on my blog:
http://yoursumbuddy.com/data-normalizer
http://yoursumbuddy.com/data-normalizer-the-sql/
Here's the code:
'Arguments
'List: The range to be normalized.
'RepeatingColsCount: The number of columns, starting with the leftmost,
' whose headings remain the same.
'NormalizedColHeader: The column header for the rolled-up category.
'DataColHeader: The column header for the normalized data.
'NewWorkbook: Put the sheet with the data in a new workbook?
'
'NOTE: The data must be in a contiguous range and the
'rows that will be repeated must be to the left,
'with the rows to be normalized to the right.
Sub NormalizeList(List As Excel.Range, RepeatingColsCount As Long, _
NormalizedColHeader As String, DataColHeader As String, _
Optional NewWorkbook As Boolean = False)
Dim FirstNormalizingCol As Long, NormalizingColsCount As Long
Dim ColsToRepeat As Excel.Range, ColsToNormalize As Excel.Range
Dim NormalizedRowsCount As Long
Dim RepeatingList() As String
Dim NormalizedList() As Variant
Dim ListIndex As Long, i As Long, j As Long
Dim wbSource As Excel.Workbook, wbTarget As Excel.Workbook
Dim wsTarget As Excel.Worksheet
With List
'If the normalized list won't fit, you must quit.
If .Rows.Count * (.Columns.Count - RepeatingColsCount) > .Parent.Rows.Count Then
MsgBox "The normalized list will be too many rows.", _
vbExclamation + vbOKOnly, "Sorry"
Exit Sub
End If
'You have the range to be normalized and the count of leftmost rows to be repeated.
'This section uses those arguments to set the two ranges to parse
'and the two corresponding arrays to fill
FirstNormalizingCol = RepeatingColsCount + 1
NormalizingColsCount = .Columns.Count - RepeatingColsCount
Set ColsToRepeat = .Cells(1).Resize(.Rows.Count, RepeatingColsCount)
Set ColsToNormalize = .Cells(1, FirstNormalizingCol).Resize(.Rows.Count, NormalizingColsCount)
NormalizedRowsCount = ColsToNormalize.Columns.Count * .Rows.Count
ReDim RepeatingList(1 To NormalizedRowsCount, 1 To RepeatingColsCount)
ReDim NormalizedList(1 To NormalizedRowsCount, 1 To 2)
End With
'Fill in every i elements of the repeating array with the repeating row labels.
For i = 1 To NormalizedRowsCount Step NormalizingColsCount
ListIndex = ListIndex + 1
For j = 1 To RepeatingColsCount
RepeatingList(i, j) = List.Cells(ListIndex, j).Value2
Next j
Next i
'We stepped over most rows above, so fill in other repeating array elements.
For i = 1 To NormalizedRowsCount
For j = 1 To RepeatingColsCount
If RepeatingList(i, j) = "" Then
RepeatingList(i, j) = RepeatingList(i - 1, j)
End If
Next j
Next i
'Fill in each element of the first dimension of the normalizing array
'with the former column header (which is now another row label) and the data.
With ColsToNormalize
For i = 1 To .Rows.Count
For j = 1 To .Columns.Count
NormalizedList(((i - 1) * NormalizingColsCount) + j, 1) = .Cells(1, j)
NormalizedList(((i - 1) * NormalizingColsCount) + j, 2) = .Cells(i, j)
Next j
Next i
End With
'Put the normal data in the same workbook, or a new one.
If NewWorkbook Then
Set wbTarget = Workbooks.Add
Set wsTarget = wbTarget.Worksheets(1)
Else
Set wbSource = List.Parent.Parent
With wbSource.Worksheets
Set wsTarget = .Add(after:=.Item(.Count))
End With
End If
With wsTarget
'Put the data from the two arrays in the new worksheet.
.Range("A1").Resize(NormalizedRowsCount, RepeatingColsCount) = RepeatingList
.Cells(1, FirstNormalizingCol).Resize(NormalizedRowsCount, 2) = NormalizedList
'At this point there will be repeated header rows, so delete all but one.
.Range("1:" & NormalizingColsCount - 1).EntireRow.Delete
'Add the headers for the new label column and the data column.
.Cells(1, FirstNormalizingCol).Value = NormalizedColHeader
.Cells(1, FirstNormalizingCol + 1).Value = DataColHeader
End With
End Sub
You’d call it like this:
Sub TestIt()
NormalizeList ActiveSheet.UsedRange, 1, "Name", "Count", False
End Sub

Resources