DataSheet
Works with a block of data on a worksheet, marked out by a start row and a start column. The block is plain cells and needs no ListObject. ColumnExists and ColumnIndex look a column up by its header, HeaderRange and DataRange answer the ranges, RenameColumn rewrites a header, FilterData and FiltersData filter on one column or on several, and Import, Export and ImportFormat move values and formats between this block and another DataSheet or a CustomTable.
WHAT THE INSTANCE HOLDS
The start row and the start column are sealed at creation. The rest of the geometry is worked out on each call: DataEndRow, DataEndColumn and the header row are read again every time they are asked for, so a caller can insert and delete rows and columns while this object stays alive. Within one call the header row is read as a single block.
Depends on: Checking, BetterArray, HiddenNames
Version: 1.0 (2026-02-09)
Factory
Create #
create
Create a DataSheet wrapper around a worksheet
Signature:
Public Function Create(ByVal sh As Worksheet, ByVal startLn As Long, _
ByVal startCl As Long, _
Optional ByVal forceEndRow As Boolean = False, _
Optional ByVal objName As String = vbNullString) As DataSheet
Entry point for creating DataSheet instances.
Wraps the supplied worksheet with convenience methods for column lookup, filtering, import/export, and formatting transfer. The data structure is identified by a starting row and column; end row and column are computed dynamically. Unlike CustomTable, no ListObject is required.
Parameters:
sh: Worksheet. The worksheet hosting the data.startLn: Long. The 1-based header row index.startCl: Long. The 1-based starting column index.forceEndRow: Optional Boolean. When True, scans all columns to find the last row. Defaults to False.objName: Optional String. Object name. Defaults to the worksheet name.
Returns: DataSheet. A fully initialised DataSheet instance.
Throws:
- ProjectError.ObjectNotInitialized When sh is Nothing.
- ProjectError.InvalidArgument When startLn or startCl is less than 1.
Depends on:
- Checking
- BetterArray
Elements
Wksh #
wksh
Worksheet backing the DataSheet
Signature:
Public Property Get Wksh() As Worksheet
Properties that expose the worksheet layout and column access.
Returns the worksheet that hosts the table-like data.
Returns: Worksheet. The host worksheet.
DataStartRow #
data-start-row
First row of data (header row)
Signature:
Public Property Get DataStartRow() As Long
Returns: Long. The 1-based header row index.
DataStartColumn #
data-start-column
First column of data
Signature:
Public Property Get DataStartColumn() As Long
Returns: Long. The 1-based starting column index.
Name #
name
Name of the DataSheet
Signature:
Public Property Get Name() As String
Returns the assigned object name. When no name was provided during creation, defaults to the worksheet name.
Returns: String. The DataSheet name.
DataEndRow #
data-end-row
Last row containing data
Signature:
Public Property Get DataEndRow() As Long
Dynamically computes the last occupied row. When StrictEnd is True, scans all non-formula columns for the furthest row. Guarantees at least one data row below the header. This is read fresh every single time. It is the value that moves most: the class itself collapses it in Clean and grows it back in Import, and callers insert and delete body rows without telling this object.
Returns: Long. The 1-based last data row index.
DataEndColumn #
data-end-column
Last column containing data
Signature:
Public Property Get DataEndColumn() As Long
Returns: Long. The 1-based last column index.
HeaderRange #
header-range
Header row Range
Signature:
Public Property Get HeaderRange() As Range
Returns the Range spanning from the start column to the end column on the header row.
Returns: Range. The header row range.
ColumnExists #
column-exists
Check whether a column exists in the DataSheet
Signature:
Public Function ColumnExists(ByVal colName As String, _
Optional ByVal strictSearch As Boolean = True, _
Optional ByVal matchCase As Boolean = True) As Boolean
Searches the header row for the specified column name. Returns False for empty column names.
Parameters:
colName: String. Column header to search for.strictSearch: Optional Boolean. When True, uses exact whole-string matching. Defaults to True.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to True.
Returns: Boolean. True when the column is found.
ColumnIndex #
column-index
Resolve the column index for a header name
Signature:
Public Function ColumnIndex(ByVal colName As String, _
Optional ByVal inDataRange As Boolean = False, _
Optional ByVal shouldExist As Boolean = False, _
Optional ByVal strictSearch As Boolean = True, _
Optional ByVal matchCase As Boolean = True) As Long
Returns the worksheet column index, or the data-range-relative index when inDataRange is True. Returns -1 when the column is not found and shouldExist is False; raises an error when shouldExist is True.
Parameters:
colName: String. Column header to look up.inDataRange: Optional Boolean. When True, returns the index relative to the data range. Defaults to False.shouldExist: Optional Boolean. When True, raises an error if not found. Defaults to False.strictSearch: Optional Boolean. When True, uses exact whole-string matching. Defaults to True.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to True.
Returns: Long. The column index, or -1 when not found.
Throws:
- ProjectError.ElementNotFound When the column is not found and shouldExist is True.
DataRange #
data-range
Range of a column or the entire data body
Signature:
Public Property Get DataRange(Optional ByVal colName As String = "__all__", _
Optional ByVal includeHeaders As Boolean = False, _
Optional ByVal strictSearch As Boolean = True, _
Optional ByVal matchCase As Boolean = True) As Range
Returns a data Range for the specified column, or the entire data body when colName is "all". When strictSearch is False, partial matching is used. Use strictSearch when column names share common text.
Parameters:
colName: Optional String. Column header to retrieve. Defaults to "all".includeHeaders: Optional Boolean. When True, includes the header row. Defaults to False.strictSearch: Optional Boolean. When True, uses exact matching. Defaults to True.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to True.
Returns: Range. The requested data range.
RenameColumn #
rename-column
Rename a header in the DataSheet
Signature:
Public Sub RenameColumn(ByVal currentName As String, _
ByVal newName As String, _
Optional ByVal strictSearch As Boolean = True, _
Optional ByVal matchCase As Boolean = False)
Replaces the header text of the matched column with newName. Raises an error when newName is empty or the column is not found.
Parameters:
currentName: String. Current column header to find.newName: String. New header text to assign.strictSearch: Optional Boolean. When True, uses exact matching. Defaults to True.matchCase: Optional Boolean. When True, matching is case-sensitive. Defaults to False.
Throws:
- ProjectError.InvalidArgument When newName is empty.
- ProjectError.ElementNotFound When the column is not found.
Operations
FilterData #
filter-data
Filter data on a single column and return the result
Signature:
Public Function FilterData(ByVal varName As String, _
ByVal criteriaName As String, _
ByVal returnedColumnName As String, _
Optional ByVal includeHeaders As Boolean = False, _
Optional ByVal matchCase As Boolean = True) _
Applies an AutoFilter on varName with criteriaName and reads the visible rows of returnedColumnName straight into a BetterArray. The filter is removed on every path, including the failure paths, so the DataSheet is left as it was found. Nothing is written to the worksheet. The routine used to copy the visible rows into a scratch block two columns past the last data column, read it back and clear it; see VisibleBlock for why that block was sized wrong and what it left behind.
Parameters:
varName: String. Column to filter on.criteriaName: String. Filter criteria to apply.returnedColumnName: String. Column whose values to return ("all" for all columns).includeHeaders: Optional Boolean. When True, includes the header row. Defaults to False.matchCase: Optional Boolean. When True, column lookup is case-sensitive. Defaults to True.
Returns: BetterArray. The filtered values.
Throws:
- ProjectError.ElementNotFound When a column name is not found.
FiltersData #
filters-data
Filter data on multiple columns and return the result
Signature:
Public Function FiltersData(ByVal varData As BetterArray, _
ByVal criteriaData As BetterArray, _
ByVal returnedColumnsData As BetterArray) As BetterArray
Applies multiple AutoFilters (one per entry in varData/criteriaData) and returns the visible values from each column in returnedColumnsData as a two-dimensional BetterArray. Exits with an empty array when varData and criteriaData have different lengths. The filters are removed on every path, including the failure paths. Nothing is written to the worksheet. Each returned column used to be pasted into its own scratch column past the last data column and the block measured back off the sheet, which had three ways to answer wrong: it gated BOTH the read and the clear on ONE cell being non-empty, so a blank first value (normal for a short-label column) dropped the whole block; it took the bottom edge from the first scratch column only; and it took the right edge from the header row, so it stopped short when the last column started blank and ran into the user's own content when anything already sat further right.
Parameters:
varData: BetterArray. Column names to filter on.criteriaData: BetterArray. Criteria corresponding to each column.returnedColumnsData: BetterArray. Columns whose values to return.
Returns: BetterArray. A two-dimensional array of filtered values.
Throws:
- ProjectError.ElementNotFound When a column name is not found.
Import #
import
Import data from another DataSheet or CustomTable
Signature:
Public Sub Import(ByVal importData As Object, Optional ByVal strictColumnSearch As Boolean = True)
Cleans this DataSheet, then iterates the source headers and copies matching column data. Formula columns are preserved. Applies format import for registered formatting columns after the data copy.
Parameters:
importData: Object. A DataSheet or CustomTable to import from.strictColumnSearch: Optional Boolean. When True, uses case-sensitive column matching. Defaults to True.
ImportFormat #
import-format
Import column formatting from a source object
Signature:
Public Sub ImportFormat(ByVal importData As Object, Optional ByVal matchColumnsCase As Boolean = True)
Copies visual formatting (colours, font weight, italic) from the source for all columns registered via AddFormatsColumns. Logs a warning when the source and destination row counts differ.
Parameters:
importData: Object. A DataSheet or CustomTable to copy formatting from.matchColumnsCase: Optional Boolean. When True, uses case-sensitive column matching. Defaults to True.
Export #
export
Export the DataSheet to a workbook
Signature:
Public Sub Export(ByVal toWkb As Workbook, Optional ByVal filteredVarName As String = "__all__", _
Optional ByVal filteredCondition As String = "<>", _
Optional ByVal Hide As Long = xlSheetHidden, _
Optional ByVal includeNames As Boolean = False)
Creates (or clears) a worksheet in the target workbook with the same name as this DataSheet, then writes all data. Optionally filters rows on a single column before export. The exported sheet is hidden by default. Applies formatting for registered columns and optionally exports hidden names.
Parameters:
toWkb: Workbook. Destination workbook.filteredVarName: Optional String. Column used for filtering. Defaults to "all" (no filter).filteredCondition: Optional String. Filter criteria. Defaults to "<>".Hide: Optional Long. Worksheet visibility after export. Defaults to xlSheetHidden.includeNames: Optional Boolean. When True, exports hidden names alongside data. Defaults to False.
Checkings
HasCheckings #
has-checkings
Whether the DataSheet has logged diagnostic messages
Signature:
Public Property Get HasCheckings() As Boolean
Returns: Boolean. True when diagnostic entries exist.
CheckingValues #
checking-values
Retrieve the diagnostic log entries
Signature:
Public Property Get CheckingValues() As Object
Returns: Object. An Checking instance, or Nothing.
AddFormatsColumns #
add-formats-columns
Register columns for formatting import/export
Signature:
Public Sub AddFormatsColumns(ByVal matchColumnsCase As Boolean, ByVal resetColumns As Boolean, ParamArray lsCols() As Variant)
Adds the specified column names to the internal formatting list so that Import and Export preserve their visual formatting. Validates each column exists before registering it.
Parameters:
matchColumnsCase: Boolean. When True, uses case-sensitive column matching.resetColumns: Boolean. When True, clears the existing list before adding.lsCols: ParamArray Variant. Column names to register for formatting.
Internal members (not exported)
Factory
Wksh #
wksh-set
Assign the worksheet reference
Signature:
Public Property Set Wksh(ByVal sh As Worksheet)
Parameters:
sh: Worksheet. The worksheet to assign.
DataStartRow #
data-start-row-set
Assign the header row index
Signature:
Public Property Let DataStartRow(ByVal startLn As Long)
Parameters:
startLn: Long. The 1-based header row index.
DataStartColumn #
data-start-column-set
Assign the starting column index
Signature:
Public Property Let DataStartColumn(ByVal startCl As Long)
Parameters:
startCl: Long. The 1-based starting column index.
StrictEnd #
strict-end-set
Assign the strict end row flag
Signature:
Public Property Let StrictEnd(ByVal forceEnd As Boolean)
Parameters:
forceEnd: Boolean. When True, scans all columns for the last row.
Name #
name-set
Assign the DataSheet name
Signature:
Public Property Let Name(ByVal objName As String)
Parameters:
objName: String. The name to assign.
Elements
StrictEnd #
strict-end
Whether to scan all columns for the last row
Signature:
Public Property Get StrictEnd() As Boolean
Returns: Boolean. True when all columns are scanned.
SheetBlock #
sheet-block
Build a Range on the host worksheet from four numbers
Signature:
Private Function SheetBlock(ByVal firstRow As Long, ByVal firstColumn As Long, _
ByVal lastRow As Long, ByVal lastColumn As Long) As Range
The "With sh ... .Range(.Cells(a, b), .Cells(c, d))" shape was written out in five places. One small builder replaces all of them.
Parameters:
firstRow: Long. Top row.firstColumn: Long. Left column.lastRow: Long. Bottom row.lastColumn: Long. Right column.
Returns: Range. The block.
RowValues #
row-values
Read a one-row Range as a 1-based list of values
Signature:
Private Function RowValues(ByVal blockRng As Range) As Variant
Range.Value hands back a plain value when the Range is one cell, and a two-dimensional array otherwise. This flattens both cases so callers see one shape and do not repeat the test.
Parameters:
blockRng: Range. A single-row block.
Returns: Variant. A 1-based one-dimensional array of cell values.
CellText #
cell-text
Read one cell value as text without raising
Signature:
Private Function CellText(ByVal cellValue As Variant) As String
A cell holding an error value (#N/A and the like) makes CStr raise a type mismatch. Such a header can never match a column name, so it reads as empty text instead of stopping the caller.
Parameters:
cellValue: Variant. The raw cell value.
Returns: String. The value as text, or empty text.
FindColumn #
find-column
Find one column in the header row, reading that row once
Signature:
Private Function FindColumn(ByVal colName As String, _
ByVal strictSearch As Boolean, _
ByVal matchCase As Boolean, _
Optional ByRef matchedHeader As String) As Long
This is the single lookup behind ColumnExists, ColumnIndex, RenameColumn and AddFormatsColumns. Each of them used to build the header Range and run Range.Find on its own, so one ColumnIndex call built the range twice and searched twice. Here the row is read once and matched in memory. The out argument carries the third answer: the header text as the sheet spells it. The four match rules are kept exactly as Range.Find applied them: strictSearch True is whole-string equality (xlWhole) and False is a substring test (xlPart); matchCase True compares binary and False compares text. The header block is deliberately NOT kept on the instance. Callers write to these sheets without going through this object, so it is read again on the next call.
Parameters:
colName: String. Column header to look for.strictSearch: Boolean. True for whole-string matching.matchCase: Boolean. True for case-sensitive matching.matchedHeader: String. Out. The header text exactly as the sheet spells it.
Returns: Long. The worksheet column of the match, or -1 when there is none.
Operations
Clean #
clean
Clear all non-formula columns
Signature:
Private Sub Clean()
Filtering, cleaning, import, and export operations.
Clears the body of every column that does not hold a formula in its first data cell. Formula columns are generated and must survive an import. The column range is built from the loop position. Looking each header text back up paid for a full column lookup on a column whose place was already known, and it had two faults: a blank header inside the range made the lookup raise and killed the whole import, and two columns sharing a header cleared the first twice and the second never. The last row is read once, before the first clear. Reading it per column shrinks it as soon as the first column is emptied, so the columns after it were left with old values below the first row.
RemoveAutoFilter #
remove-autofilter
Turn off AutoFilter on the host worksheet
Signature:
Private Sub RemoveAutoFilter()
Reliably removes any active AutoFilter. Called before and after filtering operations to guarantee a clean state. Known, and left as it is on purpose: this clears EVERY filter on the host sheet, including one the user set by hand on a ListObject of their own. The two filter routines below need the sheet unfiltered to read the right rows, and a stray filter left behind hides data from the user, which is worse than losing their view. Do not narrow this sweep without a way to prove the sheet ends up unfiltered on every path.
AreaValues #
area-values
Read one solid Range as a 1-based two-dimensional array
Signature:
Private Function AreaValues(ByVal blockRng As Range) As Variant
Range.Value hands back a plain value when the Range is a single cell and a two-dimensional array otherwise. This gives callers one shape so they do not repeat the test.
Parameters:
blockRng: Range. One solid block, never a multi-area Range.
Returns: Variant. A two-dimensional array, 1 To rows, 1 To columns.
VisibleBlock #
visible-block
Read a filtered Range as one solid 1-based two-dimensional array
Signature:
Private Function VisibleBlock(ByVal visibleRng As Range, _
ByVal firstColumn As Long, _
ByVal lastColumn As Long) As Variant
SpecialCells(xlCellTypeVisible) hands back one Area per unbroken run of visible rows, and Range.Rows.Count on such a Range answers for Areas(1) ONLY. Anything sized from that number is short as soon as the filter leaves two blocks, which is what used to break the two routines below: they sized a scratch block on the sheet from it, pasted the WHOLE selection into it (Excel anchors the paste at the top-left and writes everything, a smaller destination neither truncates nor raises), read the short block back and cleared the short block. So the caller got a truncated list and the rows past the block stayed on the worksheet. Every area is read with one .Value instead and the areas are stacked in the order Excel hands them out, which is top to bottom. Nothing is written to the worksheet, so there is no block to clear, no clipboard to reset and no fixed spot past the last column to collide with. BetterArray.FromExcelRange cannot do this job: it reads .Row, .Column, .Rows.Count and .Columns.Count off the Range - all Areas(1) answers - and then rebuilds one contiguous block from those bounds, which would pull in the hidden rows sitting inside the first area. Only the ROW runs are taken from the areas. A column hidden inside the block splits the areas sideways as well, and each row run then shows up more than once, so a run already taken is never taken twice and every run is read across the WHOLE requested column span. That also keeps a hidden column from stopping the routine: Copy raises 1004 on areas that do not line up.
Parameters:
visibleRng: Range. The visible part of a filtered block.firstColumn: Long. Left worksheet column of the block that was filtered.lastColumn: Long. Right worksheet column of the block that was filtered.
Returns: Variant. A two-dimensional array, 1 To total visible rows, 1 To columns.
StoreBlock #
store-block
Put a block into a BetterArray with the shape callers expect
Signature:
Private Sub StoreBlock(ByVal target As BetterArray, ByRef block As Variant)
This reproduces exactly what BetterArray.FromExcelRange did for the block the two filter routines used to build, and callers depend on all four cases: one cell gives a one-element array, a single column gives a FLAT one-dimensional array, a single row gives a flat one-dimensional array, and anything wider gives a two-dimensional array (BA_MULTIDIMENSION, pinned by TestDataSheet). The array is handed over in ONE Items assignment. Pushing row by row would reallocate and copy the whole array on every element.
Parameters:
target: BetterArray. The array to fill. Its LowerBound is already 1.block: Variant. A 1-based two-dimensional array.
ApplyFormat #
apply-format
Copy formatting from one Range to another
Signature:
Private Sub ApplyFormat(ByVal actRng As Range, ByVal impRng As Range)
Transfers interior colour, font colour, bold, and italic from each cell in impRng to the corresponding cell in actRng. This is not a straight format copy and xlPasteFormats would not do the same thing: only a colour that is not the default travels, so the destination keeps its own white background and black text, and borders, number formats and font names are left alone.
IsImportable #
is-importable
Test whether an object can be imported from
Signature:
Private Function IsImportable(ByVal importData As Object, ByVal whatIsImported As String) As Boolean
Import and ImportFormat both accept a DataSheet or a CustomTable and nothing else. The argument is declared As Object on purpose, so the test is on TypeName. Passing anything else logs and returns False; it does not raise, and a test drives that path with a Range.
Parameters:
importData: Object. The object handed to Import or ImportFormat.whatIsImported: String. Word used in the message ("Imports", "Imports of formats").
Returns: Boolean. True when the object can be read from.
HasFormatColumns #
has-format-columns
Test whether any column was registered for formatting
Signature:
Private Function HasFormatColumns() As Boolean
TypeName is used and not Is Nothing because it answers both "never created" and "not a BetterArray" in one test.
Returns: Boolean. True when at least one format column is registered.
Checkings
LogInfo #
log-info
Log a diagnostic message
Signature:
Private Sub LogInfo(ByVal label As String, _
Optional ByVal scope As Byte = checkingNote)
Diagnostic logging and error handling.
Adds a message to the internal Checking object, creating it on first use. The check counter provides a unique key for each entry.
Parameters:
label: String. The message to record.scope: Optional Byte. Severity level from CheckingScope. Defaults to checkingNote.
LogAndTrace #
log-and-trace
Log a message and print the debug trace
Signature:
Private Sub LogAndTrace(ByVal label As String, _
Optional ByVal scope As Byte = checkingNote)
The "log, show the trace, then leave" tail was written out six times on the failure paths of FiltersData, Import, ImportFormat and Export.
Parameters:
label: String. The message to record.scope: Optional Byte. Severity level from CheckingScope. Defaults to checkingNote.
ShowDebug #
show-debug
Print a debug trace to the Immediate window
Signature:
Private Sub ShowDebug()
ThrowError #
throw-error
Raise a ProjectError-based exception
Signature:
Private Sub ThrowError(ByVal errNumb As Long, ByVal errorMessage As String)
Parameters:
errNumb: Long. ProjectError code.errorMessage: String. Descriptive message.
Throws:
- ProjectError.
Always raises the specified error.
Seal #
seal
Seal the instance so setup setters can no longer be written
Signature:
Public Sub Seal()
GuardNotSealed #
guard-not-sealed
Guard a setup setter against writes after sealing
Signature:
Private Sub GuardNotSealed(ByVal propName As String)
Parameters:
propName: String. Name of the property being guarded.
ThrowErrorEmptySheetGiven #
throw-error-empty-sheet
Raise an error for a Nothing worksheet
Signature:
Private Sub ThrowErrorEmptySheetGiven()
Throws:
- ProjectError.ObjectNotInitialized Always.
ThrowErrorStartNumber #
throw-error-start-number
Raise an error for an invalid start row or column
Signature:
Private Sub ThrowErrorStartNumber(Optional ByVal startNumber As Long = -1, _
Optional ByVal startLabel As String = "start row")
Parameters:
startNumber: Optional Long. The invalid value. Defaults to -1.startLabel: Optional String. Label identifying the parameter. Defaults to "start row".
Throws:
- ProjectError.InvalidArgument Always.
ThrowErrorUnFoundColumn #
throw-error-unfound-column
Raise an error for a missing column
Signature:
Private Sub ThrowErrorUnFoundColumn(ByVal colName As String)
Parameters:
colName: String. The column name that was not found.
Throws:
- ProjectError.ElementNotFound Always.
Used in (19 file(s))
- ImportMetadata.cls
- LLExporter.cls
- LLImporter.cls
- LLdictionary.cls
- LLExport.cls
- LLSheets.cls
- LLVariables.cls
- CustomTable.cls
- LLChoices.cls
- Linelist.cls
- MasterSetupVariables.cls
- VarWriter.cls
- SetupImport.cls
- TestCustomPivotTable.bas
- TestImportMetadata.bas
- TestLLdictionary.bas
- TestCustomTable.bas
- TestDataSheet.bas
- TestSetupErrors.bas