Reading medium large .dat file with Excel VBA - excel

Your support is really appreciated!
I am receiving a .dat file from a measuring tool, which is found hard to get in to excel.
I would like to do it without power query as well.
I do this in steps:
Step 1; convert dat file to "csv/txt" by removing duplicate spaces and replacing spaces with ";", also replacing "." with ",".
I would like to keep this format as several other tools tends to use similar format.
And from this I thought it would be fairly ok to import it, however...
First row of 11000 rows of .dat file:
1 1 -0.4200 -0.0550 0.1420 173 174 181 56.3 55.5 59.3 87 84 95 0.778 0 0 0
first row of the converted file, all rows below looks good as well.
1;1;-0,4260;-0,1500;0,0990;171;168;176;55,5;53,8;57,6;96;83;82;4,794;0;0;0
if I import this file with power query it seems ok.
Step 2:
When importing it with the code below, following occurs on line 660
from txt file
1;660;-1,0210;-0,0340;0,0470;169;164;176;54,6;51,2;57,2;15;96;63;0,782;0;0;0
from excel:
Debuging the shows following:
file:
format of the cell is "Numbers" and not "geeral" as for other numbers.
This seems to occure now and then, and typically when the number goes above -1,xx.
Code is found online, and is fairly quick.
I suspect that something happens when putting the two-dimensional variant array into the sheet
Dim Data As Variant 'Array for the file values
.
.
.
.
With Sheets(parSheetName)
'Delete any old content
.cells.ClearContents
'A range gets the same dimensions as the array
'and the array values are inserted in one operation.
.cells(4, 1).Resize(UBound(Data, 1), UBound(Data, 2)) = Data
End With
End If
Option Explicit
'**************************************************************
' Imports CSV to sheet, following the generated numbers will be placed in a table.
'**************************************************************
Public Sub copyDataFromCsvFileToSheet(parFileName As String, _
parDelimiter As String, parSheetName As String)
Dim Data As Variant 'Array for the file values
Dim I As Long
Dim J As Long
Dim prt As String
'Function call - the file is read into the array
Data = getDataFromFile(parFileName, parDelimiter)
'If the array isn't empty it is inserted into
'the sheet in one swift operation.
If Not isArrayEmpty(Data) Then
'If you want to operate directly on the array,
'you can leave out the following lines.
ActiveWorkbook.Worksheets.Add(After:=Sheets(Sheets.Count)).Name = parSheetName
'For I = 1 To 1000 'UBound(Data, 1)
'For J = 1 To 18 'UBound(Data, 2)
''prt = Data(I, J)
''Debug.Print prt
''ThisWorkbook.Worksheets(parSheetName).cells(I, J) = Data(I, J)
'Next J
'Next I
'Debug.Print "done"
'End If
With Sheets(parSheetName)
'Delete any old content
.cells.ClearContents
'A range gets the same dimensions as the array
'and the array values are inserted in one operation.
.cells(4, 1).Resize(UBound(Data, 1), UBound(Data, 2)) = Data
End With
End If
'Call sbCreatTable(parSheetName)
End Sub
'**************************************************************
Private Function isArrayEmpty(parArray As Variant) As Boolean
'Returns False if not an array or a dynamic array
'that hasn't been initialised (ReDim) or
'deleted (Erase).
If IsArray(parArray) = False Then isArrayEmpty = True
On Error Resume Next
If UBound(parArray) < LBound(parArray) Then
isArrayEmpty = True
Exit Function
Else
isArrayEmpty = False
End If
End Function
Private Function getDataFromFile(parFileName As String, _
parDelimiter As String, _
Optional parExcludeCharacter As String = "") As Variant
'parFileName is the delimited file (csv, txt ...)
'parDelimiter is the separator, e.g. semicolon.
'The function returns an empty array, if the file
'is empty or cannot be opened.
'Number of columns is based on the line with most
'columns and not the first line.
'parExcludeCharacter: Some csv files have strings in
'quotations marks ("ABC"), and if parExcludeCharacter = """"
'quotation marks are removed.
Dim locLinesList() As Variant 'Array
Dim locData As Variant 'Array
Dim I As Long 'Counter
Dim J As Long 'Counter
Dim locNumRows As Long 'Nb of rows
Dim locNumCols As Long 'Nb of columns
Dim fso As Variant 'File system object
Dim ts As Variant 'File variable
Const REDIM_STEP = 10000 'Constant
'If this fails you need to reference Microsoft Scripting Runtime.
'You select this in "Tools" (VBA editor menu).
Set fso = CreateObject("Scripting.FileSystemObject")
On Error GoTo error_open_file
'Sets ts = the file
Set ts = fso.OpenTextFile(parFileName)
On Error GoTo unhandled_error
'Initialise the array
ReDim locLinesList(1 To 1) As Variant
I = 0
'Loops through the file, counts the number of lines (rows)
'and finds the highest number of columns.
Do While Not ts.AtEndOfStream
'If the row number Mod 10000 = 0
'we redimension the array.
If I Mod REDIM_STEP = 0 Then
ReDim Preserve locLinesList _
(1 To UBound(locLinesList, 1) + REDIM_STEP) As Variant
End If
locLinesList(I + 1) = Split(ts.ReadLine, parDelimiter)
J = UBound(locLinesList(I + 1), 1) 'Nb of columns in present row
'If the number of columns is then highest so far.
'the new number is saved.
If locNumCols < J Then locNumCols = J
I = I + 1
Loop
ts.Close 'Close file
locNumRows = I
'If number of rows is zero
If locNumRows = 0 Then Exit Function
ReDim locData(1 To locNumRows, 1 To locNumCols + 1) As Variant
'Copies the file values into an array.
'If parExcludeCharacter has a value,
'the characters are removed.
If parExcludeCharacter <> "" Then
For I = 1 To locNumRows
For J = 0 To UBound(locLinesList(I), 1)
If Left(locLinesList(I)(J), 1) = parExcludeCharacter Then
If Right(locLinesList(I)(J), 1) = parExcludeCharacter Then
locLinesList(I)(J) = _
Mid(locLinesList(I)(J), 2, Len(locLinesList(I)(J)) - 2)
Else
locLinesList(I)(J) = _
Right(locLinesList(I)(J), Len(locLinesList(I)(J)) - 1)
End If
ElseIf Right(locLinesList(I)(J), 1) = parExcludeCharacter Then
locLinesList(I)(J) = _
Left(locLinesList(I)(J), Len(locLinesList(I)(J)) - 1)
End If
locData(I, J + 1) = locLinesList(I)(J)
Next J
Next I
Else
For I = 1 To locNumRows
For J = 0 To UBound(locLinesList(I), 1)
locData(I, J + 1) = locLinesList(I)(J)
Next J
Next I
End If
getDataFromFile = locData
Exit Function
error_open_file: 'Returns empty Variant
unhandled_error: 'Returns empty Variant
End Function
Due to mentioned several measuring tools, the power query is un suited, and the control is better when using the ole way of doing it.

Solution:
Setting the variant to decimal when building the array
CDec(locLinesList(I)(J))
Thanks for your responce!

Related

Read Text file to worksheet

I have a Text file which looks like this
'52132205501000655
JAMES BOND
CC34TYU ,'006039869 , 350000, -358300.51, 0,19-04-2022, 8300.51, 0,001A
1 DAY < ACCOUNT OVERDRAWN <= 90 DAYS
'0362205501000655
WILSON JOE
CC34ZYU ,'006039869 , 550000, -358300.51, 0,19-04-2022, 8300.51, 0,001A
1 DAY < ACCOUNT OVERDRAWN <= 60DAYS
'0552205501000955
QUEEN VELVET
CDDFTYU ,'006039869 , 350000, -358300.51, 0,19-04-2022, 8300.51, 0,001A
1 DAY < ACCOUNT OVERDRAWN <= 50DAYS
I want output in a spreadsheet like MS Excel like this
'52132205501000655 JAMES BOND CC34TYU '006039869 350000 -358300.51 0 19-04-2022 8300.51 1 DAY < ACCOUNT OVERDRAWN <= 90 DAYS
which is to say that until my program encounters a blank line it should read all the values and if it contains a delimiter(, in my case) split them and put them in consecutive rows. My code reads as
Sub ReadTextFileWithSeparators()
Dim StrLine As String
Dim FSO As New FileSystemObject
Dim TSO As Object
Dim StrLineElements As Variant
Dim RowIndex As Long
Dim ColIndex As Long
Dim Delimiter As String
Set FSO = CreateObject("Scripting.FileSystemObject")
Set TSO = FSO.OpenTextFile("C:\temp\sample.txt")
Delimiter = ","
RowIndex = 1
Do While TSO.AtEndOfStream = False
StrLine = TSO.ReadLine
Do While StrLine <> vbNullString
StrLine = TSO.ReadLine
StrLineElements = Split(StrLine, Delimiter)
For ColIndex = LBound(StrLineElements) To UBound(StrLineElements)
Cells(RowIndex, ColIndex + 1).Value = StrLineElements(ColIndex)
Next ColIndex
Loop
RowIndex = RowIndex + 1
Loop
TSO.Close
Set TSO = Nothing
Set FSO = Nothing
End Sub
However i dont seem to get the desired output. Where i am doing wrong
Please, test the next code. It uses arrays and should be very fast, processing only in memory. It assumes that all text file contains groups of four lines, separate by an empty line. It will return in separate cells for each file line. The processing result will be dropped in the active sheet, starting from "A1" (header included):
Sub ReadTextFile()
Dim textFileName As String, arrTxt, arrRet, arr4Lines, arrL, arrFin, colNo As Long
Dim i As Long, j As Long, L As Long, k As Long, kk As Long, n As Long, sep As String
textFileName = "C:\temp\sample.txt"
sep = vbCrLf 'ito be changed with vbCr or vbLf if the text file will not be split on the chosen line separator
arrTxt = Split(CreateObject("Scripting.FileSystemObject").OpenTextFile(textFileName, 1).ReadAll, sep)
If UBound(arrTxt) = 0 Then MsgBox "Strange line separator..." & vbCrLf & _
"Try replacing it with 'vbCr' or 'vbLf 'and run the code again.", vbInformation, _
"Separator change needed": Exit Sub
colNo = UBound(Split(arrTxt(2), ",")) + 4 'the number of necessary columns in the final array (in a consistent txt file)
ReDim arrFin(1 To UBound(arrTxt) + 5, 1 To colNo): kk = 1 'the final array to drop its content in the sheet
For i = 0 To UBound(arrTxt) Step 5
ReDim arr4Lines(UBound(Split(arrTxt(2), ",")) * 4) 'to be sure that it is enough space to place all split elements...
For j = 0 To 3
If left(arrTxt(i + j), 1) = "=" Or arrTxt(i + j) = "" Then Exit For 'for the ending file part
arrL = Split(arrTxt(i + j), ",")
For L = 0 To UBound(arrL)
arr4Lines(k) = WorksheetFunction.Trim(arrL(L)): k = k + 1 'place in the array all the line elements (separated by comma)
Next L
Next j
If k > 0 Then
ReDim Preserve arr4Lines(k - 1) 'keep only the loaded array elements
For n = 0 To k - 1
arrFin(kk, n + 1) = arr4Lines(n) 'place the elements in the final array
Next n
kk = kk + 1 'increment the final array row
End If
Erase arr4Lines: k = 0
Next i
'drop the processed array content at once and format a little the respective range:
With ActiveSheet.Range("A2").Resize(kk - 1, colNo)
.value = arrFin
.rows(1).Offset(-1) = Array("Column1", "Column2", "Column3", "Column4", "Column5", "Column6", _
"Column7", "Column8", "Column9", "Column10", "Column11", "Column12") 'place here the necessary headers
.EntireColumn.AutoFit
End With
End Sub

My array fails to replace and offset values VBA

MyFile = Dir(MyFolder)
Do While MyFile <> ""
Application.StatusBar = "Opening" & MyFile
Set wbk = Workbooks.Open(MyFolder & MyFile, True, True)
bFound = False
For Each ws In wbk.Sheets
If ws.Name = "Sheet 1" Then
Range("B2").Select 'This gives us the first cell
Do Until IsEmpty(ActiveCell)
For i = LBound(arr, 1) To UBound(arr, 1)
For j = LBound(arr, 2) To UBound(arr, 2)
If arr(i, j) <> "" And arr(i, j) <> ActiveCell.Value Then
For k = UBound(arr, 2) To j + 1 Step -1
arr(k, i) = arr(k - 1, i)
Next k
arr(i, j) = ActiveCell.Value
End If
If arr(i, j) = "" Then
arr(i, j) = ActiveCell.Value
ActiveCell.Offset(1, 0).Select
End If
If arr(i, j) = ActiveCell.Value Then
ActiveCell.Offset(1, 0).Select
End If
If ActiveCell.Value = "" Then
Exit For
End If
Next j
If ActiveCell.Value = "" Then
Exit For
End If
Next i
Loop
End If
Next
Loop
As you can see in the image I have an array of 4 elements, I got these elements into the array by iterating through the second column of a worksheet called "Sheet 1" from a workbook called "food.xlsx" in a folder that the user choses by selection. after the array places every element from column 2 of sheet "Sheet 1" and then places these elements into column 1 of itself, our array looks like the following image...
We then move on to the next workbook called "food2.xlsx" which is located in the same folder. We look at column 2 of food2.xlsx. Column 2 of food2.xlsx has the exact same values at the exact same rows as column 2 of food.xlsx. The only difference is that in row 3 of column 2 in food2.xlsx, instead of having a value of "chocolate", there is a value of "vanilla". What am I trying to do is place "vanilla" in the location of the array where "chocolate" is currently located, this would be at arr(1,3). Then what I want is to push "chocolate" and every other value under it down one spot. So the array should end up like..
The part of the code that is NOT doing its job is the if statement that starts with "If arr(i, j) <> "" And arr(i, j) <> ActiveCell.Value Then"
IMPORTANT: I need this to work for any new encountered value, not just vanilla
Unrelated note: I prefer the FileSystemObject API over the VBA Dir function; which you can use by adding a reference (Tools -> References...) to the Microsoft Scripting Runtime library.
I would suggest using a disconnected ADO recordset. Recordsets are commonly associated with pulling data from databases or other data sources; but we can construct and fill our own, and use the recordset's built-in sorting capabilities. This frees us from worrying about shifting elements back and forth within the array, or even from the proper position in which to insert the new element.
Add a reference (Tools -> References...) to Microsoft ActiveX Data Objects; choose the latest version -- usually 6.1.
Then, you could have code like the following:
' Define the shape of the recordset
Dim rs As New ADODB.Recordset
rs.Fields.Append "Entry", adVarChar, 100
rs.Fields.Append "FileIndex", adTinyInt
rs.Fields.Append "RowIndex", adTinyInt
rs.Open
' Loop over the files, and populate the recordset
MyFile = Dir(MyFolder)
Do While MyFile <> ""
Dim fileIndex As Integer
Application.StatusBar = "Opening" & MyFile
Set wbk = Workbooks.Open(MyFolder & MyFile, True, True)
Dim data As Variant
data = wbk.Worksheets("Sheet 1").UsedRange.Columns(2).Value
Dim ubnd As Integer
ubnd = UBound(data, 1)
Dim rowIndex As Integer
For rowIndex = 1 To ubnd
Dim entry As String
entry = data(rowIndex, 1)
rs.Find "Entry='" & entry & "'"
If rs.BOF Or rs.EOF Then ' record hasn't been found or recordset is empty
rs.AddNew _
Array("Entry", "RowIndex", "FileIndex"), _
Array(entry, rowIndex, fileIndex)
rs.Update
End If
rs.MoveFirst
Next
wbk.Close
MyFile = Dir
fileIndex = fileIndex + 1
Loop
' Specify the sort order, first by the row within the file, then by the order in which
' the file was processed
rs.Sort = "RowIndex,FileIndex"
' Iterate over the data, and print it to the Immediate pane
rs.MoveFirst
Do Until rs.EOF
Debug.Print rs("Entry")
Loop
Note that the elements are sorted first by the order in which they appear in their respective files, then by the order in which the file was processed.
Links
Excel
Workbook object — Worksheets property and collection, Close method
Worksheet object — UsedRange property
Range object — Columns and Value properties
VBA
Dir, UBound functions
ADO
Recordset object
Fields property and collection, Fields.Append method
BOF / EOF properties
Find, AddNew, Update, MoveFirst methods
Sort property
Since you're determined to use an array, here's an example of how to shift "rows" down to create an empty slot at a specified index:
Sub Tester()
Dim arr
arr = Range("A1:E10").Value 'get some data
ShuffleDown arr, 3 'insert a blank row at index 3
Range("G1:K10").Value = arr 'show the modified content
End Sub
'Create a blank row at InsertRowIndex in a 2-D array, by shifting content down
'Does not warn about content being lost from the last "row"
' of the array if there's already content there !
Sub ShuffleDown(ByRef arr, InsertRowIndex As Long)
Dim rw As Long, col As Long
For rw = UBound(arr, 1) To InsertRowIndex Step -1
For col = LBound(arr, 2) To UBound(arr, 2)
If rw > InsertRowIndex Then
arr(rw, col) = arr(rw - 1, col)
Else
arr(rw, col) = ""
End If
Next col
Next rw
End Sub

Excel VBA: Sort Sheets in Alphanumeric Order

I have a workbook of about 30 sheets which I am attempting to put in alphanumeric order. Ex: "New York 9, New York 10, New York 11"
My code fails to order double digit numbers after single digit ones. "10, 11, 9"
Is anyone familiar with the method for accounting for this? Many thanks!
Sub AscendingSortOfWorksheets()
'Sort worksheets in a workbook in ascending order
Dim SCount, i, j As Integer
Application.ScreenUpdating = False
SCount = Worksheets.Count
For i = 1 To SCount - 1
For j = i + 1 To SCount
If Worksheets(j).Name < Worksheets(i).Name Then
Worksheets(j).Move before:=Worksheets(i)
End If
Next j
Next i
End Sub
As mentioned in the comments, you need to pad the numbers with zeros, in your case single digit numbers need to be padded with 1 zero. Use this function
Function PadNumber(sName As String, lNumOfDigits As Long) As String
Dim v As Variant
Dim vPrefixList As Variant
Dim sTemp As String
Dim i As Long
' Add all other possible prefixes in this array
vPrefixList = Array("New York")
sTemp = sName
For Each v In vPrefixList
sTemp = Replace(LCase(sTemp), LCase(v), "")
Next v
sTemp = Trim(sTemp)
PadNumber = sTemp
For i = Len(sTemp) + 1 To lNumOfDigits
PadNumber = "0" & PadNumber
Next i
PadNumber = Replace(sName, sTemp, PadNumber)
End Function
Then change the line If Worksheets(j).Name < Worksheets(i).Name Then to
If PadNumber(LCase(Worksheets(j).Name), 2) < PadNumber(LCase(Worksheets(i).Name), 2) Then
Note I added LCase in the comparison. Case-sensitivity might not matter for you in this particular case but it is something you always need to keep in mind.
Here is one way to achieve it
Logic:
Create a 2D array to store the number after space and sheet name
Sort the array
Arrange the sheets
Code:
Sub Sample()
Dim SheetsArray() As String
'~~> Get sheet counts
Dim sheetsCount As Long: sheetsCount = ThisWorkbook.Sheets.Count
'~~> Prepare our array for input
'~~> One part will store the number and the other will store the name
ReDim SheetsArray(1 To sheetsCount, 1 To 2)
Dim ws As Worksheet
Dim tmpAr As Variant
Dim sheetNo As Long
Dim i As Long: i = 1
Dim j As Long
'~~> Loop though the worksheest
For Each ws In ThisWorkbook.Sheets
tmpAr = Split(ws.Name)
'~~> Extract last number after space
sheetNo = Trim(tmpAr(UBound(tmpAr)))
'~~> Store number and sheet name as planned
SheetsArray(i, 1) = sheetNo
SheetsArray(i, 2) = ws.Name
i = i + 1
Next ws
'~~> Sort the array on numbers
Dim TempA, TempB
For i = LBound(SheetsArray) To UBound(SheetsArray) - 1
For j = i + 1 To UBound(SheetsArray)
If SheetsArray(i, 1) > SheetsArray(j, 1) Then
TempA = SheetsArray(j, 1): TempB = SheetsArray(j, 2)
SheetsArray(j, 1) = SheetsArray(i, 1): SheetsArray(j, 2) = SheetsArray(i, 2)
SheetsArray(i, 1) = TempA: SheetsArray(i, 2) = TempB
End If
Next j
Next i
'~~> Arrange the sheets
For i = UBound(SheetsArray) To LBound(SheetsArray) Step -1
ThisWorkbook.Sheets(SheetsArray(i, 2)).Move After:=ThisWorkbook.Sheets(sheetsCount)
sheetsCount = sheetsCount - 1
Next i
End Sub
Assumptions:
The sheet names have space in their names
The sheet names are in the format New York #

VBA excel efficient way to concatenate an array UDF

I was wondering what would be the most efficient way to create a UDF in VBA that concatenate an range from the worksheet with an additional character, let's say a comma.
I tried some variations, but I always get stuck with one problem, how to resize the array from the range selected in the worksheet automatically.
The bellow code works, but I believe there must be a more efficient way to do it.
Can you guys help me out, please?
Thanks.
Function conc(data As Range) As String
Dim hola() As Variant
t = data.Rows.Count
ReDim hola(1 To t)
a = 1
For Each i In data.Value
hola(a) = i & ","
a = a + 1
Next i
conc = Join(hola)
Erase hola
End Function
For concatenating many strings in one column and many rows (which is what your original is designed to do):
Function vconc(data As Range) As String
vconc = Join(Application.Transpose(data), Chr(44))
End Function
To concatenate many columns of strings in a single row:
Function hconc(data As Range) As String
hconc = Join(Application.Transpose(Application.Transpose(data)), Chr(44))
End Function
Don't know about more efficient. You can concatenate a specific column with
Public Function conc(ByVal data As Range) As String
conc = Join(Application.WorksheetFunction.Transpose(Application.WorksheetFunction.Index(data.Value, 0, 1)), ",")
End Function
The 1 indicates the column number of the array to concatenate.
Subject to limitations of index and transpose.
More than one column:
Public Function conc(ByVal data As Range) As String
Dim i As Long
For i = 1 To data.Columns.Count
conc = conc & Join(Application.WorksheetFunction.Transpose(Application.WorksheetFunction.Index(data.Value, 0, i)), ",")
Next i
End Function
This function I wrote some time back is pretty efficient and comprehensive...it handles 1d or 2d arrays, and you can skip blanks and add delimiters if you like. For an explanation and worked examples, see http://dailydoseofexcel.com/archives/2014/11/14/string-concatenation-is-like-the-weather/ and for a discussion on the efficiency benefits of the VBA JOIN function vs straight concatenation see http://excellerando.blogspot.com/2012/08/join-and-split-functions-for-2.html
Option Explicit
Public Function JoinText( _
InputRange As Range, _
Optional SkipBlanks As Boolean = False, _
Optional Delimiter As String = ",", _
Optional FieldDelimiter As String = ";", _
Optional EndDelimiter As String = vbNull, _
Optional Transpose As Boolean) As String
'Based on code from Nigel Heffernan at Excellerando.Blogspot.com
'http://excellerando.blogspot.co.nz/2012/08/join-and-split-functions-for-2.html
' Join up a 1 or 2-dimensional array into a string.
' ####################
' # Revision history #
' ####################
' Date (YYYYMMDD) Revised by: Changes:
' 20141114 Jeff Weir Turned into worksheet function, added FinalDelimiter and Transpose options
' 20141115 Jeff Weir Changed FinalDelimiter to EndDelimiter that accepts string, with default of ""
' 20150211 Jeff Weir Changed names of arguments and changed default orientation to Column=>Row
Dim InputArray As Variant
Dim i As Long
Dim j As Long
Dim k As Long
Dim lngNext As Long
Dim i_lBound As Long
Dim i_uBound As Long
Dim j_lBound As Long
Dim j_uBound As Long
Dim arrTemp1() As String
Dim arrTemp2() As String
If InputRange.Rows.Count = 1 Then
If InputRange.Columns.Count = 1 Then
GoTo errhandler 'InputRange is a single cell
Else
' Selection is a Row Vector
InputArray = Application.Transpose(InputRange)
End If
Else
If InputRange.Columns.Count = 1 Then
' Selection is a Column Vector
InputArray = InputRange
Transpose = True
Else:
'Selection is 2D range. Transpose it, because our
' default input is data in rows
If Not Transpose Then
InputArray = Application.Transpose(InputRange)
Else: InputArray = InputRange
End If
End If
End If
i_lBound = LBound(InputArray, 1)
i_uBound = UBound(InputArray, 1)
j_lBound = LBound(InputArray, 2)
j_uBound = UBound(InputArray, 2)
ReDim arrTemp1(j_lBound To j_uBound)
ReDim arrTemp2(i_lBound To i_uBound)
lngNext = 1
For i = j_lBound To j_uBound
On Error Resume Next
If SkipBlanks Then
If Transpose Then
ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(InputRange.Columns(i)))
Else
ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(InputRange.Rows(i)))
End If
End If
If Err.Number = 0 Then
k = 1
For j = i_lBound To i_uBound
If SkipBlanks Then
If InputArray(j, i) <> "" Then
arrTemp2(k) = InputArray(j, i)
k = k + 1
End If
Else
arrTemp2(j) = InputArray(j, i)
End If
Next j
arrTemp1(lngNext) = Join(arrTemp2, Delimiter)
lngNext = lngNext + 1
Else:
Err.Clear
End If
Next i
If SkipBlanks Then ReDim Preserve arrTemp1(1 To lngNext - 1)
If lngNext > 2 Then
JoinText = Join(arrTemp1, FieldDelimiter)
Else: JoinText = arrTemp1(1)
End If
If JoinText <> "" Then JoinText = JoinText & EndDelimiter
errhandler:
End Function

VBA Excel 2-Dimensional Arrays

I was trying to find out how to declare a 2-Dimensional array but all of the examples I have found so far are declared with set integers. I'm trying to create a program that will utilize two 2-Dimensional arrays and then perform simple operations on those arrays (such as finding difference or percent). The arrays are populated by numbers in Excel sheets (one set of numbers is on Sheet1 and another set is on Sheet2, both sets have the same number of rows and columns).
Since I don't know how many rows or columns there are I was going to use variables.
Dim s1excel As Worksheet
Dim s2excel As Worksheet
Dim s3excel As Worksheet
Dim firstSheetName As String
Dim secondSheetName As String
Dim totalRow As Integer
Dim totalCol As Integer
Dim iRow As Integer
Dim iCol As Integer
Set s1excel = ThisWorkbook.ActiveSheet
' Open the "Raw_Data" workbook
Set wbs = Workbooks.Open(file_path & data_title)
wbs.Activate
ActiveWorkbook.Sheets(firstSheetName).Select
Set s2excel = wbs.ActiveSheet
' Find totalRow, totalColumn (assumes there's values in Column A and Row 1 with no blanks)
totalRow = ActiveSheet.Range("A1").End(xlDown).Row
totalCol = ActiveSheet.Range("A1").End(xlToRight).Column
Dim s2Array(totalRow, totalCol)
Dim s3Array(totalRow, totalCol)
For iRow = 1 To totalRow
For iCol = 1 To totalCol
s2Array(iRow, iCol) = Cells(iRow, iCol)
Next iCol
Next iRow
ActiveWorkbook.Sheets(secondSheetName).Select
Set s3excel = wbs.ActiveSheet
For iRow = 1 To totalRow
For iCol = 1 To totalCol
s3Array(iRow, iCol) = Cells(iRow, iCol)
Next iCol
Next iRow
When I attempt to run this I get a compile-time error at the Dim s2Array(totalRow, totalCol) saying that a constant expression is required. The same error occurs if I change it to Dim s2Array(1 To totalRow, 1 To totalCol). Since I don't know what the dimensions are from the get go I can't declare it like Dim s2Array(1, 1) because then I'll get an out-of-bounds exception.
Thank you,
Jesse Smothermon
In fact I would not use any REDIM, nor a loop for transferring data from sheet to array:
dim arOne()
arOne = range("A2:F1000")
or even
arOne = range("A2").CurrentRegion
and that's it, your array is filled much faster then with a loop, no redim.
You need ReDim:
m = 5
n = 8
Dim my_array()
ReDim my_array(1 To m, 1 To n)
For i = 1 To m
For j = 1 To n
my_array(i, j) = i * j
Next
Next
For i = 1 To m
For j = 1 To n
Cells(i, j) = my_array(i, j)
Next
Next
As others have pointed out, your actual problem would be better solved with ranges. You could try something like this:
Dim r1 As Range
Dim r2 As Range
Dim ws1 As Worksheet
Dim ws2 As Worksheet
Set ws1 = Worksheets("Sheet1")
Set ws2 = Worksheets("Sheet2")
totalRow = ws1.Range("A1").End(xlDown).Row
totalCol = ws1.Range("A1").End(xlToRight).Column
Set r1 = ws1.Range(ws1.Cells(1, 1), ws1.Cells(totalRow, totalCol))
Set r2 = ws2.Range(ws2.Cells(1, 1), ws2.Cells(totalRow, totalCol))
r2.Value = r1.Value
Here's A generic VBA Array To Range function that writes an array to the sheet in a single 'hit' to the sheet. This is much faster than writing the data into the sheet one cell at a time in loops for the rows and columns... However, there's some housekeeping to do, as you must specify the size of the target range correctly.
This 'housekeeping' looks like a lot of work and it's probably rather slow: but this is 'last mile' code to write to the sheet, and everything is faster than writing to the worksheet. Or at least, so much faster that it's effectively instantaneous, compared with a read or write to the worksheet, even in VBA, and you should do everything you possibly can in code before you hit the sheet.
A major component of this is error-trapping that I used to see turning up everywhere . I hate repetitive coding: I've coded it all here, and - hopefully - you'll never have to write it again.
A VBA 'Array to Range' function
Public Sub ArrayToRange(rngTarget As Excel.Range, InputArray As Variant)
' Write an array to an Excel range in a single 'hit' to the sheet
' InputArray must be a 2-Dimensional structure of the form Variant(Rows, Columns)
' The target range is resized automatically to the dimensions of the array, with
' the top left cell used as the start point.
' This subroutine saves repetitive coding for a common VBA and Excel task.
' If you think you won't need the code that works around common errors (long strings
' and objects in the array, etc) then feel free to comment them out.
On Error Resume Next
'
' Author: Nigel Heffernan
' HTTP://Excellerando.blogspot.com
'
' This code is in te public domain: take care to mark it clearly, and segregate
' it from proprietary code if you intend to assert intellectual property rights
' or impose commercial confidentiality restrictions on that proprietary code
Dim rngOutput As Excel.Range
Dim iRowCount As Long
Dim iColCount As Long
Dim iRow As Long
Dim iCol As Long
Dim arrTemp As Variant
Dim iDimensions As Integer
Dim iRowOffset As Long
Dim iColOffset As Long
Dim iStart As Long
Application.EnableEvents = False
If rngTarget.Cells.Count > 1 Then
rngTarget.ClearContents
End If
Application.EnableEvents = True
If IsEmpty(InputArray) Then
Exit Sub
End If
If TypeName(InputArray) = "Range" Then
InputArray = InputArray.Value
End If
' Is it actually an array? IsArray is sadly broken so...
If Not InStr(TypeName(InputArray), "(") Then
rngTarget.Cells(1, 1).Value2 = InputArray
Exit Sub
End If
iDimensions = ArrayDimensions(InputArray)
If iDimensions < 1 Then
rngTarget.Value = CStr(InputArray)
ElseIf iDimensions = 1 Then
iRowCount = UBound(InputArray) - LBound(InputArray)
iStart = LBound(InputArray)
iColCount = 1
If iRowCount > (655354 - rngTarget.Row) Then
iRowCount = 655354 + iStart - rngTarget.Row
ReDim Preserve InputArray(iStart To iRowCount)
End If
iRowCount = UBound(InputArray) - LBound(InputArray)
iColCount = 1
' It's a vector. Yes, I asked for a 2-Dimensional array. But I'm feeling generous.
' By convention, a vector is presented in Excel as an arry of 1 to n rows and 1 column.
ReDim arrTemp(LBound(InputArray, 1) To UBound(InputArray, 1), 1 To 1)
For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
arrTemp(iRow, 1) = InputArray(iRow)
Next
With rngTarget.Worksheet
Set rngOutput = .Range(rngTarget.Cells(1, 1), rngTarget.Cells(iRowCount + 1, iColCount))
rngOutput.Value2 = arrTemp
Set rngTarget = rngOutput
End With
Erase arrTemp
ElseIf iDimensions = 2 Then
iRowCount = UBound(InputArray, 1) - LBound(InputArray, 1)
iColCount = UBound(InputArray, 2) - LBound(InputArray, 2)
iStart = LBound(InputArray, 1)
If iRowCount > (65534 - rngTarget.Row) Then
iRowCount = 65534 - rngTarget.Row
InputArray = ArrayTranspose(InputArray)
ReDim Preserve InputArray(LBound(InputArray, 1) To UBound(InputArray, 1), iStart To iRowCount)
InputArray = ArrayTranspose(InputArray)
End If
iStart = LBound(InputArray, 2)
If iColCount > (254 - rngTarget.Column) Then
ReDim Preserve InputArray(LBound(InputArray, 1) To UBound(InputArray, 1), iStart To iColCount)
End If
With rngTarget.Worksheet
Set rngOutput = .Range(rngTarget.Cells(1, 1), rngTarget.Cells(iRowCount + 1, iColCount + 1))
Err.Clear
Application.EnableEvents = False
rngOutput.Value2 = InputArray
Application.EnableEvents = True
If Err.Number <> 0 Then
For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)
If IsNumeric(InputArray(iRow, iCol)) Then
' no action
Else
InputArray(iRow, iCol) = "" & InputArray(iRow, iCol)
InputArray(iRow, iCol) = Trim(InputArray(iRow, iCol))
End If
Next iCol
Next iRow
Err.Clear
rngOutput.Formula = InputArray
End If 'err<>0
If Err <> 0 Then
For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)
If IsNumeric(InputArray(iRow, iCol)) Then
' no action
Else
If Left(InputArray(iRow, iCol), 1) = "=" Then
InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
End If
If Left(InputArray(iRow, iCol), 1) = "+" Then
InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
End If
If Left(InputArray(iRow, iCol), 1) = "*" Then
InputArray(iRow, iCol) = "'" & InputArray(iRow, iCol)
End If
End If
Next iCol
Next iRow
Err.Clear
rngOutput.Value2 = InputArray
End If 'err<>0
If Err <> 0 Then
For iRow = LBound(InputArray, 1) To UBound(InputArray, 1)
For iCol = LBound(InputArray, 2) To UBound(InputArray, 2)
If IsObject(InputArray(iRow, iCol)) Then
InputArray(iRow, iCol) = "[OBJECT] " & TypeName(InputArray(iRow, iCol))
ElseIf IsArray(InputArray(iRow, iCol)) Then
InputArray(iRow, iCol) = Split(InputArray(iRow, iCol), ",")
ElseIf IsNumeric(InputArray(iRow, iCol)) Then
' no action
Else
InputArray(iRow, iCol) = "" & InputArray(iRow, iCol)
If Len(InputArray(iRow, iCol)) > 255 Then
' Block-write operations fail on strings exceeding 255 chars. You *have*
' to go back and check, and write this masterpiece one cell at a time...
InputArray(iRow, iCol) = Left(Trim(InputArray(iRow, iCol)), 255)
End If
End If
Next iCol
Next iRow
Err.Clear
rngOutput.Text = InputArray
End If 'err<>0
If Err <> 0 Then
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
iRowOffset = LBound(InputArray, 1) - 1
iColOffset = LBound(InputArray, 2) - 1
For iRow = 1 To iRowCount
If iRow Mod 100 = 0 Then
Application.StatusBar = "Filling range... " & CInt(100# * iRow / iRowCount) & "%"
End If
For iCol = 1 To iColCount
rngOutput.Cells(iRow, iCol) = InputArray(iRow + iRowOffset, iCol + iColOffset)
Next iCol
Next iRow
Application.StatusBar = False
Application.ScreenUpdating = True
End If 'err<>0
Set rngTarget = rngOutput ' resizes the range This is useful, *most* of the time
End With
End If
End Sub
You will need the source for ArrayDimensions:
This API declaration is required in the module header:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
...And here's the function itself:
Private Function ArrayDimensions(arr As Variant) As Integer
'-----------------------------------------------------------------
' will return:
' -1 if not an array
' 0 if an un-dimmed array
' 1 or more indicating the number of dimensions of a dimmed array
'-----------------------------------------------------------------
' Retrieved from Chris Rae's VBA Code Archive - http://chrisrae.com/vba
' Code written by Chris Rae, 25/5/00
' Originally published by R. B. Smissaert.
' Additional credits to Bob Phillips, Rick Rothstein, and Thomas Eyde on VB2TheMax
Dim ptr As Long
Dim vType As Integer
Const VT_BYREF = &H4000&
'get the real VarType of the argument
'this is similar to VarType(), but returns also the VT_BYREF bit
CopyMemory vType, arr, 2
'exit if not an array
If (vType And vbArray) = 0 Then
ArrayDimensions = -1
Exit Function
End If
'get the address of the SAFEARRAY descriptor
'this is stored in the second half of the
'Variant parameter that has received the array
CopyMemory ptr, ByVal VarPtr(arr) + 8, 4
'see whether the routine was passed a Variant
'that contains an array, rather than directly an array
'in the former case ptr already points to the SA structure.
'Thanks to Monte Hansen for this fix
If (vType And VT_BYREF) Then
' ptr is a pointer to a pointer
CopyMemory ptr, ByVal ptr, 4
End If
'get the address of the SAFEARRAY structure
'this is stored in the descriptor
'get the first word of the SAFEARRAY structure
'which holds the number of dimensions
'...but first check that saAddr is non-zero, otherwise
'this routine bombs when the array is uninitialized
If ptr Then
CopyMemory ArrayDimensions, ByVal ptr, 2
End If
End Function
Also: I would advise you to keep that declaration private. If you must make it a public Sub in another module, insert the Option Private Module statement in the module header. You really don't want your users calling any function with CopyMemoryoperations and pointer arithmetic.
For this example you will need to create your own type, that would be an array. Then you create a bigger array which elements are of type you have just created.
To run my example you will need to fill columns A and B in Sheet1 with some values. Then run test(). It will read first two rows and add the values to the BigArr. Then it will check how many rows of data you have and read them all, from the place it has stopped reading, i.e., 3rd row.
Tested in Excel 2007.
Option Explicit
Private Type SmallArr
Elt() As Variant
End Type
Sub test()
Dim x As Long, max_row As Long, y As Long
'' Define big array as an array of small arrays
Dim BigArr() As SmallArr
y = 2
ReDim Preserve BigArr(0 To y)
For x = 0 To y
ReDim Preserve BigArr(x).Elt(0 To 1)
'' Take some test values
BigArr(x).Elt(0) = Cells(x + 1, 1).Value
BigArr(x).Elt(1) = Cells(x + 1, 2).Value
Next x
'' Write what has been read
Debug.Print "BigArr size = " & UBound(BigArr) + 1
For x = 0 To UBound(BigArr)
Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
Next x
'' Get the number of the last not empty row
max_row = Range("A" & Rows.Count).End(xlUp).Row
'' Change the size of the big array
ReDim Preserve BigArr(0 To max_row)
Debug.Print "new size of BigArr with old data = " & UBound(BigArr)
'' Check haven't we lost any data
For x = 0 To y
Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
Next x
For x = y To max_row
'' We have to change the size of each Elt,
'' because there are some new for,
'' which the size has not been set, yet.
ReDim Preserve BigArr(x).Elt(0 To 1)
'' Take some test values
BigArr(x).Elt(0) = Cells(x + 1, 1).Value
BigArr(x).Elt(1) = Cells(x + 1, 2).Value
Next x
'' Check what we have read
Debug.Print "BigArr size = " & UBound(BigArr) + 1
For x = 0 To UBound(BigArr)
Debug.Print BigArr(x).Elt(0) & " | " & BigArr(x).Elt(1)
Next x
End Sub

Resources