Separate data by month - excel

I have an employer list indicating the reinforcement shifts per dd/mm/yyyy in an Excel 2003 workbook.
With the next macro I get in the same document, all the GP per person multiplied by 4.83 indicating the result in a new column.
Option Explicit
Sub Resumen()
'------------------
'by Cacho Rodríguez
'------------------
Dim C As Range, Mat, Q&, i&, R&
On Error Resume Next
Set C = Application.InputBox("Selecciona la celda superior izquierda (CODIGO NÓMINA)" & vbLf & _
"de tu rango de datos." & vbLf & vbLf & "(por ejemplo: Full1!$A$1)", Type:=8)
If C Is Nothing Then Exit Sub
On Error GoTo 0
Application.ScreenUpdating = False
With C.Worksheet
Mat = .Range(C, .Cells(.Rows.Count, 1 + C.Column).End(xlUp).Offset(, 1))
End With
Q = UBound(Mat)
R = 1
Mat(R, 1) = Mat(1, 1)
Mat(R, 2) = Mat(1, 2)
Mat(R, 3) = "GP"
For i = 2 To Q
Select Case True
Case Mat(i, 1) = ""
Mat(R, 3) = 1 + Mat(R, 3)
Case IsNumeric(Mat(i, 1))
R = 1 + R
Mat(R, 1) = 0 + Mat(i, 1)
Mat(R, 2) = Mat(i, 2)
Mat(R, 3) = 0
End Select
Next
C.Worksheet.[g1].CurrentRegion.Delete xlUp
With C.Worksheet.[g1].Resize(R, 3)
Application.Goto .Cells(1).Offset(, -3), True
.Value = Mat
.Columns(4) = "=4.83 * " & .Cells(1, 3).Address(0, 0)
.Cells(1, 4) = "Total"
.Resize(, 4).Columns.AutoFit
End With
Application.ScreenUpdating = True
End Sub
I need too all GF in a new column and in the "Total column", the result of GP+GF*4.83.
But I need the GP and GF separate per month, and the total per month per employer.
For example something like the next picture:

It took a bit for me to figure out what you want to do. If I understand properly: your 3rd image is a Summary of the data in the 1st image, and you want it to also include the data from your 2nd image.
If this is going to be an ongoing report then your first step should be organizing the data better, which will then make this and anything else you ever want to do with this data in Excel a lot easier for you and others.
If your data were organized like this:
...then with just a few clicks, you can have you data displayed like this automatically:
...and any time you add or change data, it take 1 click to update this table. It only took a few minutes to create this pivot table (now that the data is organized properly).
One in place, the pivot table can be changed with only a few clicks to instantly report on the data in different ways.
Same goes for Charts (which took a couple minutes to create, and will automatically update when the data changes) and various other Excel features:
You're doing things the "hard way" by using VBA to create your reports -- but it's very common from users who aren't aware of the functionality already built-in to Excel. But as I said, the first step in organizing your data in a more logical fashion (basically, "one record per row" with no sub-headings in between rows, like the Nom on your sample data.)
If you'd like to play around with the workbook I used for the examples, you can download it from Jumpshare here. (It probably won't display properly on the JumpShare website (because of the charts, etc) but click the Download button to download the [macro-free] .XLSX file.
More Information:
Microsoft : Guidelines for organizing and formatting data on a worksheet
Hubspot : How to Create a Pivot Table in Excel: A Step-by-Step Tutorial (With Video)
Office.com : Create a PivotTable to analyze worksheet data
GCFLearnFree: Introduction to Pivot Tables (with practice workbook)

Here is a macro that will reorder the data you have into a more useable format as recommended by #ashleedawg. The macro makes use of two Classes to help with orgainizing, and the self-documenting feature will be useful for future modifications.
After you have reordered your data, you can then apply pivot tables to generate whatever type of report you wish. For the 4,83 multiplier, you can add a Calculated Field to the Pivot Table.
And, you can even record a macro to automate the generation of the Pivot Table, if you wish.
For information on Classes, take a look at Chip Pearson's Introduction to Classes
As noted in the comments in the various modules:
Be sure to set a Reference to Microsoft Scripting Runtime
Be sure to rename the Class Modules
Be sure your worksheets for the original and results data are named appropriately in the reOrder macro
Class Module
Option Explicit
'RENAME cShiftData
Private pCodigo As Long
Private pNom As String
Private pDt As Date
Private pDNI As String
Private pGP As Double
Private pGF As Double
Private pSD As cShiftData
Private pDts As Dictionary
Public Property Get Codigo() As Long
Codigo = pCodigo
End Property
Public Property Let Codigo(Value As Long)
pCodigo = Value
End Property
Public Property Get Nom() As String
Nom = pNom
End Property
Public Property Let Nom(Value As String)
pNom = 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 DNI() As String
DNI = pDNI
End Property
Public Property Let DNI(Value As String)
pDNI = Value
End Property
Public Property Get GP() As Double
GP = pGP
End Property
Public Property Let GP(Value As Double)
pGP = Value
End Property
Public Property Get GF() As Double
GF = pGF
End Property
Public Property Let GF(Value As Double)
pGF = Value
End Property
Public Property Get Dts() As Dictionary
Set Dts = pDts
End Property
Public Function addDtsItem(dat As Date)
If Dts.Exists(dat) Then
MsgBox "Duplicate key will not be added"
Else
Dim V
Set pSD = New cShiftData
With pSD
.GF = Me.GF
.GP = Me.GP
End With
Dts.Add dat, pSD
End If
End Function
Private Sub Class_Initialize()
Set pDts = New Dictionary
End Sub
Class Module
Option Explicit
'RENAME cDateData
Private pGP As Double
Private pGF As Double
Public Property Get GP() As Double
GP = pGP
End Property
Public Property Let GP(Value As Double)
pGP = Value
End Property
Public Property Get GF() As Double
GF = pGF
End Property
Public Property Let GF(Value As Double)
pGF = Value
End Property
Regular Module
Option Explicit
'SET REFERENCE TO: Microsoft Scripting Runtime
Sub reOrder()
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant
Dim cSD As cShiftData, dSD As Dictionary
Dim I As Long, J As Long
Dim V As Variant, W As Variant
'set source and results worksheets
'read data into array
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 1)
Set wsSrc = Worksheets("Sheet1")
V = LastRowCol(wsSrc.Name)
With wsSrc
vSrc = .Range(.Cells(1, 1), .Cells(V(0), V(1)))
End With
'collect the data
Set dSD = New Dictionary
For I = 1 To UBound(vSrc, 1)
If Not vSrc(I, 1) Like "*CODIGO*" And _
Len(vSrc(I, 1)) > 0 Then 'start of a new codigo
Set cSD = New cShiftData
With cSD
.Codigo = vSrc(I, 1)
.Nom = vSrc(I, 2)
.DNI = vSrc(I, 3)
dSD.Add Key:=.Codigo, Item:=cSD
End With
ElseIf Len(vSrc(I, 1)) = 0 Then
With cSD
.Dt = vSrc(I, 2)
.GP = vSrc(I, 4)
.GF = vSrc(I, 5)
dSD(.Codigo).addDtsItem (.Dt)
End With
End If
Next I
'create results array
'one line for each date
I = 0
For Each V In dSD.Keys
I = I + dSD(V).Dts.Count
Next V
ReDim vRes(0 To I, 1 To 6)
'Header row
vRes(0, 1) = "CODIGO NOMINA"
vRes(0, 2) = "NOM"
vRes(0, 3) = "D.N.I."
vRes(0, 4) = "FECHA"
vRes(0, 5) = "GP"
vRes(0, 6) = "GF"
I = 0
For Each V In dSD.Keys
With dSD(V)
For Each W In .Dts
I = I + 1
vRes(I, 1) = .Codigo
vRes(I, 2) = .Nom
vRes(I, 3) = .DNI
vRes(I, 4) = W
vRes(I, 5) = .Dts(W).GP
vRes(I, 6) = .Dts(W).GF
Next W
End With
Next V
'write the results
Application.ScreenUpdating = False
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
.Columns(4).NumberFormat = "dd/mm/yyyy"
.Columns(1).HorizontalAlignment = xlCenter
With .EntireColumn
.ColumnWidth = 255
.AutoFit
End With
End With
myPivot wsRes
Application.ScreenUpdating = True
End Sub
Function LastRowCol(Worksht As String) As Long()
Application.Volatile
Dim WS As Worksheet, R As Range
Dim LastRow As Long, LastCol As Long
Dim L(1) As Long
Set WS = Worksheets(Worksht)
With WS
Set R = .Cells.Find(what:="*", after:=.Cells(1, 1), _
LookIn:=xlValues, searchorder:=xlByRows, _
searchdirection:=xlPrevious)
If Not R Is Nothing Then
LastRow = R.Row
LastCol = .Cells.Find(what:="*", after:=.Cells(1, 1), _
LookIn:=xlValues, searchorder:=xlByColumns, _
searchdirection:=xlPrevious).Column
Else
LastRow = 1
LastCol = 1
End If
End With
L(0) = LastRow
L(1) = LastCol
LastRowCol = L
End Function
Original Data
reOrdered Data (after running macro)
Example Pivot Table

Related

List box view, search when typing and double click select

I'm trying to create a data entry solution to move people away from interacting with excel sheets as a short term fix. I've put together the below using various bits of code that I found online, which allows me to double click on a row and the data gets populated in the text boxes to allow for editing.
Image of data entry box using the code below
Here's the code for the double click on list box and display data in the text boxes:
Private Sub datadisplay_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Me.TextBox13.Value = Me.datadisplay.List(Me.datadisplay.ListIndex, 18)
Me.TextBox1.Value = Me.datadisplay.List(Me.datadisplay.ListIndex, 0)
Me.TextBox2.Value = Me.datadisplay.List(Me.datadisplay.ListIndex, 1)
Me.TextBox3.Value = Me.datadisplay.List(Me.datadisplay.ListIndex, 2)
Me.TextBox4.Value = Me.datadisplay.List(Me.datadisplay.ListIndex, 3)
Here's the code to display the data and headers in the list box:
Private Sub Refresh_Data()
Dim sh As Worksheet
Set sh = ThisWorkbook.Sheets("Reach_Out")
Dim last_Row As Long
last_Row = Application.WorksheetFunction.CountA(sh.Range("B:B"))
With Me.datadisplay
.ColumnHeads = True
.ColumnCount = 21
.ColumnWidths = "70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,70,140,70"
If last_Row = 1 Then
.RowSource = "Reach_Out!B2:U2"
Else
.RowSource = "Reach_Out!B2:U" & last_Row
End If
End With
End Sub
Here's the code for the criteria box that allows me to select a column header that will allow me to search the column based on what is selected
Private Sub UserForm_Initialize()
Dim c As Integer
For c = 2 To 19
Me.ComboBox4.AddItem Sheet2.Cells(1, c).Value
Next
End Sub
I've tried to use this for the search box, but it only populates one column of data rather than two as expected.
Private Sub textsearch_Change()
Dim sh As Worksheet
Set sh = Sheets("Reach_Out")
Dim i As Long
Dim x As Long
Dim p As Long
Me.datadisplay.Clear
For i = 2 To sh.Range("B" & Rows.Count).End(xlUp).Row
For x = 1 To Len(sh.Cells(i, 2))
p = Me.textsearch.TextLength
If LCase(Mid(sh.Cells(i, 2), x, p)) = Me.textsearch And Me.textsearch <> "" Then
With Me.datadisplay
.AddItem sh.Cells(i, 2)
.List(datadisplay.ListCount - 1, 1) = sh.Cells(i, 3)
.List(datadisplay.ListCount - 1, 2) = sh.Cells(i, 4)
End With
End If
Next x
Next i
Call Refresh_Data
End Sub
Search box only showing one column rather than two
The problem is putting all of this together as the data and column headers extracted into the list box doesn't work with the search box and double click code. Does anybody have any ideas on how I can merge this all together into one bit of code please?

Minimum and Maxiumum of like values

I am trying to take a range of values in a number of sets, and write a VBA code to fill in grouped cells with the minimum and maximum values in the range.
Example Table
Group
Value
A
10
A
3
A
5
B
1
B
3
The expected outputs should be a table or cells that say
A Range : 3-10
B Range : 1-3
Obviously, I could do this with a min and max formula, for each set, however, I'm wondering if there's a way to do this in VBA as the number of sets is in excess of a few hundred
You can certainly do this with VBA and/or Power Query, although, as others have written and you are aware, formulas are a viable option.
Source Data
for VBA:
Read the source data into a VBA array for fastest processing
create a class module to hold the minumum and maximum values
Group the ranges using a Dictionary, which in turn holds the class object
Create a results array and write it to the worksheet.
Class Module
'ReNAME this "cGroup"
Option Explicit
Private pMin As Long
Private pMax As Long
Public Property Get Min() As Long
Min = pMin
End Property
Public Property Let Min(Value As Long)
pMin = Value
End Property
Public Property Get Max() As Long
Max = pMax
End Property
Public Property Let Max(Value As Long)
pMax = Value
End Property
Regular Module
'Set reference to Microsoft Scripting Runtime
Option Explicit
Sub generateRanges()
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant, v As Variant
Dim I As Long
Dim D As Dictionary, sKey As String
Dim cG As cGroup
'set the source and results worksheets
Set wsSrc = ThisWorkbook.Worksheets("sheet1")
Set wsRes = ThisWorkbook.Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 1)
'read source data into vba array
With wsSrc
vSrc = Range(.Cells(1, 1), .Cells(.Rows.Count, 2).End(xlUp))
End With
'set dictionary to collect the data
Set D = New Dictionary
D.CompareMode = TextCompare
'iterate through the data
'pull out the min and max of the range
For I = 2 To UBound(vSrc, 1)
sKey = vSrc(I, 1)
Set cG = New cGroup
If Not D.Exists(sKey) Then
cG.Max = vSrc(I, 2)
cG.Min = vSrc(I, 2)
D.Add Key:=sKey, Item:=cG
Else
With D(sKey)
.Max = IIf(.Max > vSrc(I, 2), .Max, vSrc(I, 2))
.Min = IIf(.Min < vSrc(I, 2), .Min, vSrc(I, 2))
End With
End If
Next I
'create the results array
ReDim vRes(0 To D.Count, 1 To 2)
'Column Headers
vRes(0, 1) = "Range"
vRes(0, 2) = "Value"
I = 0
For Each v In D.Keys
I = I + 1
vRes(I, 1) = v & " Range"
vRes(I, 2) = D(v).Min & "-" & D(v).Max
Next v
'write results to results worksheet
With rRes.Resize(UBound(vRes, 1) + 1, UBound(vRes, 2))
.EntireColumn.Clear
.NumberFormat = "#"
.Value = vRes
.Style = "Output" 'not internationally aware
.EntireColumn.AutoFit
End With
End Sub
Or, using Power Query (which I prefer because its shorter and easier to program) available in Windows Excel 2010+ and Office 365:
Select some cell in your Data Table
Data => Get&Transform => from Table/Range
When the PQ Editor opens: Home => Advanced Editor
Make note of the Table Name in Line 2
Paste the M Code below in place of what you see
Change the Table name in line 2 back to what was generated originally.
Read the comments and explore the Applied Steps to understand the algorithm
M Code
let
//Read in the table
// Change Table name in next line to actual table name
Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
//type the data
#"Changed Type" = Table.TransformColumnTypes(Source,{{"Group", type text}, {"Value", Int64.Type}}),
//Group by "Group"
// then generate the min-max string
#"Grouped Rows" = Table.Group(#"Changed Type", {"Group"}, {{"Value",
each Text.From(List.Min([Value])) & "-" & Text.From(List.Max([Value])), Text.Type }
}),
//Add the word " Range" to the Group
addRange = Table.TransformColumns(#"Grouped Rows",{"Group", each _ & " Range", Text.Type})
in
addRange
Either produces the same results from your data:
Indeed, VBA isn't the best solution in this case because a single formula can extract all the data you want. There is no need to make a different formula for each group.
=MINIFS(INDEX(Data,,2),INDEX(Data,,1),H2) & " - " & MAXIFS(INDEX(Data,,2),INDEX(Data,,1),H2)
The formula for the dynamic named range is =OFFSET(Sheet1!$A$2,0,0,COUNTA(Sheet1!$A:$A)-1,2)
If you want to go with formula then could try below formula with Excel365.
D2=UNIQUE(A2:A6)
E2=MINIFS(B2:B6,A2:A6,D2#)&"-"&MAXIFS(B2:B6,A2:A6,D2#)

Conditional copying from a table in Excel

I'm trying to copy to both debit/credit columns to other tables which match only the respective account value i.e. all Cash entries go to a Cash Account table, etc. I'll also need a way to omit those that have already been copied (so some check column will have to be referenced).
but I'm unclear how to translate this into VBA.
Here's a visual from the worksheet:
And my VBA code so far (MyAdd being a function that copies the range to another specified table)
Sub CopyRange()
For Each c In Range("Journal").Cells
If c.Value = "Cash" Then
If Range("Journal[#[Account 1]]").Value = "Cash" Then MyAdd "Cash_Account", Range(c.Offset(0, 2), c.Offset(0, 3))
Else: MyAdd "Cash_Account", Range(c.Offset(0, 1), c.Offset(0, 2))
Next
End Sub
I'm not sure why you'd want to do this. It would seem there is another end goal in mind. However, to do what you're asking in VBA can be done with the below code.
Sub GetNewColumnOfData()
Dim Table As ListObject
Dim TargetRange As Range
Dim Index As Long
Dim Values As Variant
Set Table = ThisWorkbook.Worksheets("Sheet3").ListObjects("Journal")
Set TargetRange = ThisWorkbook.Worksheets("Sheet3").Range("G1")
ReDim Values(1 To Table.ListRows.Count, 1 To 1)
For Index = 1 To Table.ListRows.Count
If Table.ListColumns("Account 1").DataBodyRange(Index, 1).Value = "Cash" Then
Values(Index, 1) = 1
ElseIf Table.ListColumns("Account 2").DataBodyRange(Index, 1).Value = "Cash" Then
Values(Index, 1) = 2
End If
Next Index
TargetRange.Resize(Table.ListRows.Count, 1).Value = Values
End Sub
Define your range/table names accordingly.
Using Zack's solution, I have created my solution this way - in case anyone wants to follow my work and improve upon it:
Sub GetNewColumnOfData()
Dim Table As ListObject
Dim TargetRange As Range
Dim Index As Long
Dim Account As String
Set Table = Range("Journal").ListObject
For Index = 1 To Table.ListRows.Count
If Not IsEmpty(Table.ListColumns("Account 1").DataBodyRange(Index, 1)) And IsEmpty(Table.ListColumns("*").DataBodyRange(Index, 1)) Then
Account = Table.ListColumns("Account 1").DataBodyRange(Index, 1).Value
Table.ListColumns("*").DataBodyRange(Index, 1).Value = "*"
MyAdd Account, Range(Table.ListColumns("Debit").DataBodyRange(Index, 1), Table.ListColumns("Credit").DataBodyRange(Index, 1))
ElseIf Not IsEmpty(Table.ListColumns("Account 2").DataBodyRange(Index, 1)) And IsEmpty(Table.ListColumns("*").DataBodyRange(Index, 1)) Then
Account = Table.ListColumns("Account 2").DataBodyRange(Index, 1).Value
Table.ListColumns("*").DataBodyRange(Index, 1).Value = "*"
MyAdd Account, Range(Table.ListColumns("Debit").DataBodyRange(Index, 1), Table.ListColumns("Credit").DataBodyRange(Index, 1))
End If
Next Index
End Sub
The MyAdd function was derived elsewhere on this site but I quote it here for ease of reference:
Sub MyAdd(ByVal strTableName As String, ByRef arrData As Variant)
Dim tbl As ListObject
Dim NewRow As ListRow
Set tbl = Range(strTableName).ListObject
Set NewRow = tbl.ListRows.Add(AlwaysInsert:=True)
' Handle Arrays and Ranges
If TypeName(arrData) = "Range" Then
NewRow.Range = arrData.Value
Else
NewRow.Range = arrData
End If
End Sub
Note I put this code in a module for the Workbook - and all the Ranges (tables/lists) are by default Workbook named ranges - hence accessible without needing to specify the sheets they are on.

Excel vba nested dictionary - accessing items

Tim is it possible to extract a list of row keys from the clsMatrix class? something like this...
Sub KEYS()
Dim KEY_LIST As Variant
KEY_LIST = TABLES("UDLY").dR.KEYS
End Sub
I can then cycle through a table to extract a subset of data which meet a certain criteria.
Tim, your code works well for one 2D matrix, but I have 5 tables to reference for the project to work. I tried using if...then else statements, but it's clumsy and doesn't work - the second pass looking for data from the BOOK table can't find the row and col dictionary references. Can you suggest a better method? Thanks for your help.
Option Explicit
Private dR, dC
Private m_arr, UDLY, BOOK
'
Sub Init(TABLE As String)
Dim i As Long
Dim RNGE As Range
Dim DATA As Variant
Dim arr As Variant
If TABLE = "UDLY" Then Set RNGE = Worksheets("SETTINGS").Range("UDLY_TABLE")
If TABLE = "BOOK" Then Set RNGE = Worksheets("BOOK").Range("BOOK_TABLE")
arr = RNGE.Value
Set dR = CreateObject("Scripting.Dictionary")
Set dC = CreateObject("Scripting.Dictionary")
'add the row keys and positions
For i = LBound(arr, 1) + 1 To UBound(arr, 1)
dR.Add arr(i, 1), i
Next i
'add the column keys and positions
For i = LBound(arr, 2) + 1 To UBound(arr, 2)
dC.Add arr(1, i), i
Next i
' m_arr = arr
If TABLE = "UDLY" Then UDLY = arr
If TABLE = "BOOK" Then BOOK = arr
End Sub
Function GetValue(TABLE, rowKey, colKey)
If dR.Exists(rowKey) And dC.Exists(colKey) Then
' GetValue = m_arr(dR(rowKey), dC(colKey))
If TABLE = "UDLY" Then GetValue = UDLY(dR(rowKey), dC(colKey))
If TABLE = "BOOK" Then GetValue = BOOK(dR(rowKey), dC(colKey))
Else
GetValue = 999 '"" 'or raise an error...
End If
End Function
'===========================================================
Option Explicit
Sub Tester()
Dim m As New clsMatrix
' m.Init (ActiveSheet.Range("b40").CurrentRegion.Value)
' m.Init (Worksheets("settings").Range("udly_table"))
m.Init ("UDLY")
Debug.Print m.GetValue("UDLY", "APZ4-FUT", "SPOT_OFFLINE")
m.Init ("BOOK")
Debug.Print m.GetValue("BOOK", "2.04", "STRIKE")
End Sub
Sub DICT_OF_DICT()
Dim d1, d2
Set d1 = CreateObject("Scripting.Dictionary")
Set d2 = CreateObject("Scripting.Dictionary")
d1.Add "BPH", "Hello"
d2.Add "Shaun", d1
Debug.Print d2("Shaun").Item("BPH")
End Sub
EDIT: if I wanted to deal with quickly accessing a 2-D array using row/column headers then I'd be inclined not to use nested dictionaries, but to use two distinct dictionaries to key into each dimension (a "row label" dictionary and a "column label" one).
You can wrap this up in a simple class:
'Class module: clsMatrix
Option Explicit
Private dR, dC
Private m_arr
Sub Init(arr)
Dim i As Long
Set dR = CreateObject("Scripting.Dictionary")
Set dC = CreateObject("Scripting.Dictionary")
'add the row keys and positions
For i = LBound(arr, 1) + 1 To UBound(arr, 1)
dR.Add arr(i, 1), i
Next i
'add the column keys and positions
For i = LBound(arr, 2) + 1 To UBound(arr, 2)
dC.Add arr(1, i), i
Next i
m_arr = arr
End Sub
Function GetValue(rowKey, colKey)
If dR.Exists(rowKey) And dC.Exists(colKey) Then
GetValue = m_arr(dR(rowKey), dC(colKey))
Else
GetValue = "" 'or raise an error...
End If
End Function
'EDIT: added functions to return row/column keys
' return a zero-based array
Function RowKeys()
RowKeys = dR.Keys
End Function
Function ColumnKeys()
ColumnKeys = dC.Keys
End Function
Example usage: assuming A1 is the top-left cell in a rectangular range where the first row is column headers ("col1" to "colx") and the first column is row headers ("row1" to "rowy") -
EDIT2: made some changes to show how to manage multiple different tables (with no changes to the class code)
'Regular module
Sub Tester()
Dim tables As Object, k
Set tables = CreateObject("Scripting.Dictionary")
tables.Add "Table1", New clsMatrix
tables("Table1").Init ActiveSheet.Range("A1").CurrentRegion.Value
tables.Add "Table2", New clsMatrix
tables("Table2").Init ActiveSheet.Range("H1").CurrentRegion.Value
Debug.Print tables("Table1").GetValue("Row1", "Col3")
Debug.Print tables("Table2").GetValue("R1", "C3")
k = tables("Table1").RowKeys()
Debug.Print Join(k, ", ")
End Sub

Data transformation from Excel

I have some data in Excel file with some horizontal and vertical dimensions. It looks like this:
This data has to be loaded into some BI system. For this purpose I have to transform data to the "table style". In other words it should be presented in table like this:
I need some effective algorithm to make this transformation. The only one which I know is to take value from first cell (100000) get values from vertical and horizontal coordinates (Russia, Population, 1900) and insert into first row. Then take another cell and so on.
It would be work with small amount of data, but with big amount it works very slowly. Do you know more sophisticated algorithm for this kind of data?
There are several ways of doing this with VBA. In this solution, I first create a user defined Object named Country, with four properties: Name, Index, YR, and Quantity. It is not necessary to do this; but I've been working with these recently and I think it adds some clarity to the code.
I then read the Source data into a VBA array (which can be done in a single step), iterate through the array to create a collection of Country objects.
I then go through the Country collection, outputting the properties into a Results array, where I want them.
Finally, the results array is outputted to a worksheet -- again, just a single step.
One could go directly from the Source data array to the Results array, but I think it is easier to see what's going on using the object.
One could also not bother with the VBA array, but process the cells directly from one worksheet to another. In my experience, this approach will be at least an order of magnitude slower than using the VBA array approach.
Depending on the size of your database, refinements may be necessary. Be sure to read the comments in the code.
To define the Country object, insert a Class Module and rename it Country.
Place the following code in that module:
==========================================
Option Explicit
Private pName As String
Private pIndex As String
Private pYr As Long
Private pQuantity As Double
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
Public Property Get Index() As String
Index = pIndex
End Property
Public Property Let Index(Value As String)
pIndex = Value
End Property
Public Property Get Yr() As Long
Yr = pYr
End Property
Public Property Let Yr(Value As Long)
pYr = Value
End Property
Public Property Get Quantity() As Double
Quantity = pQuantity
End Property
Public Property Let Quantity(Value As Double)
pQuantity = Value
End Property
=============================================
Then, Insert a regular module and place this code there:
=======================================
Option Explicit
Sub TransformData()
Dim wsSrc As Worksheet 'Data Source
Dim wsRes As Worksheet, rRes As Range 'Results go here
Dim vSrc As Variant 'Actual data goes into this array
Dim vRes() As Variant 'Results will go here before being written to worksheet
Dim cCTY As Country 'User defined object
Dim colCountries As Collection
Dim I As Long, J As Long 'counters
Set wsSrc = Worksheets("Sheet2") '<--change these to whatever
Set wsRes = Worksheets("Sheet3")
Set rRes = wsRes.Range("A1") '<--1st cell of results array
'read data into array
With wsSrc
vSrc = .Range("A1").CurrentRegion '<--many ways to get this depending on your real data setup
End With
'iterate through Source and create collection of results
Set colCountries = New Collection
For I = 2 To UBound(vSrc, 1) '<--Rows
For J = 3 To UBound(vSrc, 2) '<--Columns
Set cCTY = New Country
With cCTY
.Name = vSrc(I, 1)
.Index = vSrc(I, 2)
.Yr = vSrc(1, J)
.Quantity = vSrc(I, J)
End With
colCountries.Add cCTY
Next J
Next I
'Results
ReDim vRes(0 To colCountries.Count, 1 To 4)
'Column Labels
vRes(0, 1) = "Country"
vRes(0, 2) = "Index"
vRes(0, 3) = "Year"
vRes(0, 4) = "Value"
For I = 1 To colCountries.Count
With colCountries(I)
vRes(I, 1) = .Name
vRes(I, 2) = .Index
vRes(I, 3) = .Yr
vRes(I, 4) = .Quantity
End With
Next I
Set rRes = rRes.Resize(UBound(vRes, 1) + 1, UBound(vRes, 2))
rRes.EntireColumn.Clear
rRes = vRes
With rRes.Rows(1)
.Font.Bold = True
.HorizontalAlignment = xlCenter
End With
rRes.EntireColumn.AutoFit
End Sub
====================================================
Ensure the worksheets and ranges are properly defined to accord with your real setup, and run the macro.

Resources