TableSpecs

Holds one row of an analysis setup ListObject and answers what the table that row defines looks like. TableScope says which kind of table it is, TableId and TableSectionId name it, ValidTable says whether the row can be built and IsNewSection whether it opens a section. HasTotal, TotalRequested, HasMissing, HasPercentage and HasGraph answer the flags, GraphMode and FlipCoordinates the graph settings, and Value reads any column of the row. Previous and NextSpecs walk to the neighbouring rows. GeoCount, SpatialTableScopes, SpatialPrefix, SpatialVariableName, SpatialConcatVariableName and SpatialLabelColumn describe a spatial table.

WHERE THE SCOPE COMES FROM

The scope is the name of the ListObject the row sits in. The seven analysis tables carry seven fixed names and each name maps to one scope. An earlier version read a type label from a fixed row offset above the header. That offset is four rows in the workbook shipped from src/bin/setup and two rows in every published release. A ListObject name holds the same fact and it stays put when a row is inserted.

WHAT IS HELD

Column names, cell values and the table scope are read once at construction. ValidTable, IsNewSection, TableId, TableSectionId and SpatialTableScopes are worked out on the first ask and held. Seal runs at the end of Create and stops the ranges changing after that, so an answer held on an instance stays true for its whole life.

CATEGORY LOOKUPS

RowCategories and ColumnCategories take an Object. A LinelistSpecs resolves the categories from its dictionary and its choices, and any other object gives an empty BetterArray. That is what the setup error checks pass, where the geo and translation managers are absent. LinelistSpecs is reached through a TypeName string, so it stays outside the compile dependencies of this class.

Depends on: LLdictionary, LLVariables, BetterArray, Checking, --- Column and cell value caches ---, --- Scalar caches ---, Names of the seven analysis ListObjects on the setup worksheet. Each name, is one table scope. Six other classes hold the same strings beside the code, that reads them, which is the house rule: a class keeps every constant it, needs inside itself., Column search terms. Every one is a partial, case-insensitive match against, the header row, so "row" finds "Group by variable (row)"., Column names read through LLVariables, on the dictionary sheet., Geo prefixes written by LLdictionary.AppendGeoLines., The concatenated form of an administrative variable, which the spatial, formulas summarise over. Health facility tables have no concatenated form and, read the plain prefix in the same position., Dictionary columns carrying the label of a spatial variable. An, administrative level is labelled from its sub section, a health facility from, its main label., VBA raises this when a late-bound object has no such member., Bounds for the "N geo max" column, which is free text on the setup sheet.

Version: 3.0 (2026-07-29)

Instantiation

Create #

create

Create a TableSpecs instance from a setup row

Signature:

Public Function Create(ByVal headerRng As Range, _
                       ByVal tableSpecsRng As Range, _
                       ByVal dict As LLdictionary, _
                       Optional ByVal vars As LLVariables) As TableSpecs

Factory method that creates a new TableSpecs instance representing one row from an analysis setup ListObject. Each row in the setup table defines a single analysis table (e.g. a univariate frequency table or a time series). The TableSpecs object wraps that row and exposes properties to query its scope, validity, navigation to neighboring rows, and display flags (total, missing, percentage, graph). Category lookups (RowCategories, ColumnCategories) are resolved later at call time by passing a LinelistSpecs object. Validation is performed immediately via CheckRequirements before the instance is populated. After property injection, InitCache reads the header and the data row with two array reads and resolves the table scope from the ListObject name.

Parameters:

  • headerRng: Range. The single header row Range of the analysis setup ListObject. Column names in this range are used by Value() to locate fields such as "row", "column", "total", "section", etc.
  • tableSpecsRng: Range. A single data-row Range from the same ListObject. Must sit below headerRng, start at the same column and have the same number of columns.
  • dict: LLdictionary. The linelist dictionary that provides variable definitions, existence checks, and control type lookups needed for validation and spatial type detection.
  • vars: LLVariables. Optional reader over the same dictionary. Pass one when the caller already holds it: an analysis run then builds a single LLVariables for every row instead of one per row and per category lookup. When it is omitted the class builds its own on first use.

Returns: TableSpecs. A fully initialised TableSpecs instance ready for use.

Throws:

  • InvalidArgument When headerRng is Nothing.
  • InvalidArgument When tableSpecsRng is Nothing.
  • InvalidArgument When dict is Nothing.
  • InvalidArgument When headerRng and tableSpecsRng have different column counts.
  • InvalidArgument When headerRng and tableSpecsRng start at different columns.
  • InvalidArgument When either range spans more than one row.
  • InvalidArgument When headerRng.Row is not above tableSpecsRng.Row.
  • ErrorUnexpectedState When the specification row sits outside a known analysis table.

Cache Initialization

InitCache #

init-cache

Populate the column and value caches and resolve the scope

Signature:

Public Sub InitCache()

One-time cache population called from the factory.

Reads the header range and the specification row with one array read each and builds parallel BetterArray caches of column names and cell values. Then resolves the table scope from the name of the ListObject the row sits in. Runs exactly once, from Create, before Seal: a later call raises, because the derived caches held elsewhere in the class are computed from this data.

Throws:

  • SomethingWentWrong When called after the instance is sealed.
  • ErrorUnexpectedState When the specification row sits outside a known analysis table.

Core Properties

TableScope #

table-scope

Analysis table scope for this specification row

Signature:

Public Property Get TableScope() As AnalysisTableScope

Type identification and identity properties for each table spec.

Returns the cached AnalysisTableScope enum value. The scope is resolved once during InitCache from the name of the ListObject the specification row sits in. On subsequent calls the cached Long value is returned directly without any worksheet access.

Returns: AnalysisTableScope. The table scope category.


TableId #

table-id

Unique identifier for this analysis table

Signature:

Public Property Get TableId() As String

Generates a unique string identifier by combining a short type-based prefix with the row offset from the header. The prefix is derived from the TableScope: "GS" for GlobalSummary, "UA" for Univariate, "BA" for Bivariate, "TS" for TimeSeries, "SA" for Spatial, "SPT" for SpatioTemporal and "TSG" for the time series graph block. The row offset is computed as TableRange.Row minus HeaderRange.Row, producing a stable integer that uniquely locates this row within its analysis block. The resulting format is "prefix_tabN", for example "UA_tab3". Seal fixes both ranges, so the answer is computed once and held.

Returns: String. The unique table identifier such as "GS_tab1" or "BA_tab5".

Throws:

  • ErrorUnexpectedState When the table scope has no prefix.

TableSectionId #

table-section-id

Identifier of the first table in this section

Signature:

Public Property Get TableSectionId() As String

Returns the TableId of the first table in the current section group by walking backward through the Previous chain. If this table is itself the start of a new section (IsNewSection = True) or is a GlobalSummary table, it returns its own TableId immediately. The same answer is given when Previous finds no valid table left in the section. Otherwise it delegates to Previous.TableSectionId, which continues walking backward until a section boundary is reached. All tables within the same section share a common section identifier.

Returns: String. The TableId of the first table in this section.

Remarks:

  • This is a recursive property. Resolving the section id of the k-th table in a section builds k instances, and a caller that walks a whole block row by row pays that once per row. A caller iterating in order can carry the section id in a local and avoid the walk entirely.

Dictionary #

dictionary

Linelist dictionary bound to this specification

Signature:

Public Property Get Dictionary() As LLdictionary

Returns the LLdictionary reference stored at construction time. The dictionary provides variable existence checks, control type lookups, and spatial prefix detection used throughout validation and category resolution.

Returns: LLdictionary. The linelist dictionary instance.


GeoCount #

geo-count

Number of geographic units this table shows

Signature:

Public Property Get GeoCount() As Long

Reads the "N geo max" column and answers a usable count. The column is free text on the setup sheet, so the value is clamped: anything that is not a number, and an empty cell, answer the default of 5; a value below 1 answers 1 and a value above 20 answers 20. Without the clamp a "0" builds a table with no columns and a large value builds a table that wide.

Returns: Long. The number of geographic units, between 1 and 20.


SpatialTableScopes #

spatial-table-types

Spatial analysis sub-type

Signature:

Public Property Get SpatialTableScopes() As String

Determines the spatial sub-type of a Spatial or SpatioTemporal table by checking whether the relevant variable has a health-facility or administrative geography prefix in the dictionary. For Spatial tables, the variable comes from the "row" field; for SpatioTemporal tables, the spatial variable is in the "column" field. The method prepends "hf_" or "adm1_" to the variable name and checks whether that prefixed variable exists in the dictionary. Returns "hf" for health facility, "geo" for administrative geography, or vbNullString if neither prefix is found. The answer is held after the first call: it costs up to two dictionary searches and nine production call sites ask for it.

Returns: String. The spatial sub-type ("hf", "geo", or empty string).

Remarks:

  • This property is meaningful only for Spatial and SpatioTemporal table scopes. Calling it on other table scopes will still execute but will likely return vbNullString since those variables typically lack geo/hf prefixes.

SpatialPrefix #

spatial-prefix

Dictionary prefix for this table's spatial variable

Signature:

Public Property Get SpatialPrefix() As String

The prefix SpatialTableScopes probed with, handed back so callers stop rebuilding it. Four sites across two classes wrote "adm1_" and "hf_" beside their own copy of the geo-against-facility test, which is how the tag that names the spatial input cells came to disagree with the tag the formulas referenced.

Returns: String. "hf_", "adm1_", or an empty string when neither prefix matched.


SpatialVariableName #

spatial-variable-name

Dictionary variable holding the spatial value

Signature:

Public Property Get SpatialVariableName(ByVal baseVar As String) As String

The prefixed variable name the dictionary answers for, which is the prefix above joined to a variable read off this specification row.

Parameters:

  • baseVar: String. The unprefixed variable name from the spec row.

Returns: String. For example "adm1_zone", or an empty string when this table has no spatial type.


SpatialConcatVariableName #

spatial-concat-variable-name

Dictionary variable the spatial formulas read

Signature:

Public Property Get SpatialConcatVariableName(ByVal baseVar As String) As String

The formula writer reads a different variable from the table writer: an administrative table is summarised over the concatenated column, while a health facility table uses the same variable in both places. That asymmetry is the whole reason this is a second member rather than a prefix argument.

Parameters:

  • baseVar: String. The unprefixed variable name from the spec row.

Returns: String. For example "concat_adm1_zone" or "hf_facility", or an empty string when this table has no spatial type.


SpatialLabelColumn #

spatial-label-column

Dictionary column carrying the label of the spatial variable

Signature:

Public Property Get SpatialLabelColumn() As String

An administrative level is labelled from its sub section and a health facility from its main label. The two travel with the prefix at every call site, so a caller that asked only for the prefix would still have to branch.

Returns: String. "sub section", "main label", or an empty string when this table has no spatial type.


Validation Properties

ValidTable #

valid-table

Whether this table has a valid configuration

Signature:

Public Property Get ValidTable() As Boolean

Properties that validate table configuration and detect section boundaries.

Returns the cached validation result. On first access, validates whether this table specification row contains all required fields and references valid variables for its analysis type. Each table scope has different validation rules: GlobalSummary requires non-empty "label" and "function" fields; Univariate requires the "row" variable to be a choice variable; Bivariate requires both "row" and "column" to be choice variables; TimeSeries requires the "row" variable to be of type "date"; Spatial requires a geographic prefix in the dictionary; SpatioTemporal requires a geographic prefix on the column variable and a date-type row variable; the time series graph block requires a graph title and a series title, the two columns the user picks from a dropdown. Invalid table specs are skipped during navigation (Previous, NextSpecs).

Returns: Boolean. True if the table specification meets all requirements for its type.

Depends on:

  • LLVariables

IsNewSection #

is-new-section

Whether this table begins a new section group

Signature:

Public Property Get IsNewSection() As Boolean

Returns the cached section-boundary result. On first access, determines whether this table specification row begins a new section within the analysis setup block. Sections group related tables together. The method reads the "section" column of the nearest row above this one that the build will draw, and answers True when this row's section differs from it. A row with no valid row above it starts a section too, which covers the first data row of the block and the row whose section anchor failed validation. GlobalSummary tables are never considered new sections regardless of their section value, because each GlobalSummary table is standalone and does not participate in section grouping.

Returns: Boolean. True if this table starts a new section.

Remarks:

  • The row physically above may be one the build skips. Comparing against it made a section whose anchor row was invalid unopenable, so the comparison walks back to the nearest valid row instead. The section column is free text, so the two values are matched without regard to case.

Flag Properties

HasTotal #

has-total

Whether totals are needed for computation

Signature:

Public Property Get HasTotal() As Boolean

Properties controlling optional table features: totals, missing, percentage, and graphs.

Determines whether a total row/column is needed for this table. The logic varies by table scope: GlobalSummary, SpatioTemporal and the time series graph block always return False; Univariate and Bivariate always return True; TimeSeries returns True when the user explicitly requested total="yes" or when percentage is set to "row" or "column" (because percentage formulas need the total column as a denominator); Spatial returns True only when a column variable is specified. This is distinct from TotalRequested: HasTotal can be True even when the user did not ask for totals, solely because percentage needs them.

Returns: Boolean. True if a total row/column should be created.

Throws:

  • ErrorUnexpectedState When the table scope is unmapped.

TotalRequested #

total-requested

Whether total was explicitly requested by the user

Signature:

Public Property Get TotalRequested() As Boolean

Returns True only when the user explicitly wrote "yes" in the "total" column of the setup row. This is the companion to HasTotal for the visibility logic: when HasTotal is True but TotalRequested is False, the total column is created for percentage computation but hidden from the user's view. If TotalRequested is True, the total column is both created and visible.

Returns: Boolean. True if the "total" field equals "yes".

Remarks:

  • For Univariate and Bivariate tables, TotalRequested is not consulted because HasTotal is unconditionally True and totals are always shown. Its one production caller reads it for time series tables.

HasMissing #

has-missing

Whether missing-data rows or columns are included

Signature:

Public Property Get HasMissing() As Boolean

Determines whether a "missing" row or column should be included in the output table. The logic varies by table scope: GlobalSummary, SpatioTemporal and the time series graph block always return False; Univariate returns True when missing="yes"; Bivariate returns True when missing="row", "column", or "all"; TimeSeries and Spatial return True when missing="yes" and a column variable is present (without a column variable, there is no categorical axis to show missing values on).

Returns: Boolean. True if missing data should be shown in the output table.

Throws:

  • ErrorUnexpectedState When the table scope is unmapped.

HasPercentage #

has-percentage

Whether percentage display is enabled

Signature:

Public Property Get HasPercentage() As Boolean

Determines whether percentage values should be computed and displayed for this table. The logic varies by table scope: GlobalSummary, SpatioTemporal and the time series graph block always return False; Univariate returns True when percentage="yes"; Bivariate returns True when percentage="row", "column", or "total"; TimeSeries returns True when percentage="row" or "column" and HasTotal is True; Spatial returns True when percentage="yes" and HasTotal is True. The interplay between HasPercentage and HasTotal for TimeSeries is intentional: requesting percentage implicitly forces HasTotal=True since percentage needs the total.

Returns: Boolean. True if percentage values should be included.

Throws:

  • ErrorUnexpectedState When the table scope is unmapped.

HasGraph #

has-graph

Whether a graph should be created for this table

Signature:

Public Property Get HasGraph() As Boolean

Determines whether a chart/graph should be generated alongside the output table. The logic varies by table scope: GlobalSummary and the time series graph block always return False; Univariate, TimeSeries, Spatial, and SpatioTemporal return True when graph="yes"; Bivariate returns True when graph="percentages" or "values", the two entries of the __perc_val dropdown the setup binds to that column. The actual graph rendering is handled downstream by the chart builder; this property only signals whether one should be created. "both" stays in the bivariate list because GraphSeries reads it too; the dropdown offers "percentages" and "values".

Returns: Boolean. True if a graph should be created for this table.

Remarks:

  • The time series graph block answers False. Every one of its rows is already a graph, built by WriteTimeSeriesGraphs, and the term "graph" matches its "Graph title (select)" column, which holds a title.

Throws:

  • ErrorUnexpectedState When the table scope is unmapped.

GraphMode #

graph-mode

The kind of chart this table asks for

Signature:

Public Property Get GraphMode() As String

Answers the "add graph" cell trimmed and lower-cased, which is the same reading HasGraph uses to decide whether a chart is wanted at all. The chart builder reads it to choose between value bars, percentage bars and both, so the two answers come from one place. A cell holding " Yes " reads "yes" here.

Returns: String. One of "yes", "values", "percentages", "both", or an empty string when the column is absent or the cell is empty.


FlipCoordinates #

flip-coordinates

Whether the chart plots horizontal bars

Signature:

Public Property Get FlipCoordinates() As Boolean

Answers the "flip coordinates" cell trimmed and lower-cased, compared against "yes". The spatio-temporal block carries no such column, so it answers False there.

Returns: Boolean. True when the table asks for horizontal bars.


Data Access

Value #

value

Retrieve a value from the specification row by column name

Signature:

Public Function Value(ByVal colName As String, _
                      Optional ByVal strictSearch As Boolean = False) As String

Value retrieval and category lookups from the specification row.

Retrieves the cached cell value from this table specification row for a given column name. By default the column is located by scanning the cached column names for a case-insensitive substring match, so "row" finds "Group by variable (row)". Pass strictSearch:=True to require the whole header to match the term, which is what a caller wants when two headers on the same sheet share a word. Once found, the corresponding value is returned from the parallel value cache without any worksheet access. A column name absent from the header returns vbNullString.

Parameters:

  • colName: String. The column name to search for in the header.
  • strictSearch: Boolean. Optional. True compares the whole header against colName. The default False keeps the substring match every existing caller relies on.

Returns: String. The cell value, or vbNullString if the column is not found.


RowCategories #

row-categories

Row variable categories from the dictionary

Signature:

Public Property Get RowCategories(ByRef lData As Object) As BetterArray

Returns a BetterArray of category values for the row variable of this table. When lData is a LinelistSpecs instance, the categories are retrieved from it based on the variable name in the "row" field. When lData is any other type (or Nothing), returns an empty BetterArray. Internally delegates to CategoriesData("row", lData).

Parameters:

  • lData: Object. A LinelistSpecs instance providing category lookups, or any other Object (categories will be empty).

Returns: BetterArray. The category strings for the row variable, or an empty BetterArray if the row variable is not found or lData is not LinelistSpecs.


ColumnCategories #

column-categories

Column variable categories from the dictionary

Signature:

Public Property Get ColumnCategories(ByRef lData As Object) As BetterArray

Returns a BetterArray of category values for the column variable of this table. When lData is a LinelistSpecs instance, the categories come from it based on the variable name in the "column" field. When lData is any other type (or Nothing), returns an empty BetterArray. For SpatioTemporal tables, the column categories are empty placeholder strings whose count comes from GeoCount. These placeholders are filled in later at runtime with actual geographic location names. Internally delegates to CategoriesData("column", lData).

Parameters:

  • lData: Object. A LinelistSpecs instance providing category lookups, or any other Object (categories will be empty).

Returns: BetterArray. The category strings for the column variable. For SpatioTemporal, contains GeoCount empty strings. Returns an empty BetterArray if the column variable is not found or not specified.


Internal members (not exported)

Instantiation

Seal #

seal

Seal the instance against further setup writes.

Signature:

Public Sub Seal()

GuardNotSealed #

guard-not-sealed

Guard a setup routine against post-creation calls.

Signature:

Private Sub GuardNotSealed(ByVal propName As String)

Parameters:


Internal Properties

HeaderRange #

header-range

Header range of the setup ListObject

Signature:

Public Property Get HeaderRange() As Range

Properties used by the factory pattern during initialisation.

Returns the header row Range that contains column names such as "row", "column", "total", "section", etc. Used by Value() for all field lookups.

Returns: Range. The header row range.


HeaderRange #

header-range-set

Assign the header range

Signature:

Public Property Set HeaderRange(ByVal hRng As Range)

Parameters:


TableRange #

table-range

Table specification row range

Signature:

Public Property Get TableRange() As Range

Returns the single data-row Range from the setup ListObject that defines this table specification.

Returns: Range. The table specification row range.


TableRange #

table-range-set

Assign the table specification row range

Signature:

Public Property Set TableRange(ByVal tRng As Range)

Parameters:


DictionaryObject #

dictionary-object-set

Assign the dictionary object

Signature:

Public Property Set DictionaryObject(ByVal dict As LLdictionary)

The read side of this pair is the Dictionary property below, which is the one every consumer uses.

Parameters:


Variables #

variables-set

Assign the variable reader handed in by the caller

Signature:

Public Property Set Variables(ByVal vars As LLVariables)

Set from the factory with whatever Create received. Passing Nothing leaves the class to build its own reader on first use.

Parameters:


Helpers

CellText #

cell-text

Coerce one cell value to text, surviving an error value

Signature:

Private Function CellText(ByVal cellValue As Variant) As String

Private helper methods that support data access and validation.

A cell holding #REF!, #N/A, #VALUE! or #NAME? answers an Error variant, and CStr on an Error variant raises runtime error 13. The analysis setup sheet carries formula columns, so an ordinary edit can produce one. An error value, a Null and an empty cell all answer an empty string here.

Parameters:

Returns: String. The text of the cell, or vbNullString.


FlagValue #

flag-value

Read a flag column, trimmed and lower-cased

Signature:

Private Function FlagValue(ByVal colName As String) As String

The flag columns carry values picked from a dropdown, so the normal path is already clean. A value pasted into the cell bypasses Excel validation, so every flag comparison goes through here and reads " Yes " the same way it reads "yes".

Parameters:

Returns: String. The flag value, trimmed and lower-cased.


VariablesObject #

variables-object

The variable reader over this specification's dictionary

Signature:

Private Function VariablesObject() As LLVariables

Answers the LLVariables handed to Create, or builds one on first use and holds it for the life of the instance. LLVariables.Create requires a fully-configured dictionary (a Data sheet and a "variable name" column). The setup error-checking context may not expose those, so a failure here answers Nothing and the callers fall back. The attempt is made once.

Returns: LLVariables. The variable reader, or Nothing when one cannot be built.


ColumnIndex #

column-index

Return the 1-based column index of a named header column

Signature:

Private Function ColumnIndex(ByVal colName As String, _
                             Optional ByVal strictSearch As Boolean = False) As Long

Scans the cached column names for the search term. The default is a case-insensitive substring match; strictSearch compares the whole header. Returns the 1-based position within the header range, or -1 if not found.

Parameters:

Returns: Long. The 1-based position within the header range, or -1 if not found.


CategoriesData #

categories-data

Build a BetterArray of categories for a row or column variable

Signature:

Private Function CategoriesData(ByVal rowOrCol As String, _
                                ByRef lData As Object) As BetterArray

Shared implementation for RowCategories and ColumnCategories. Reads the variable name from the specified field using Value(rowOrCol), then asks lData for the categories of that variable. An object that supplies Categories answers them; anything else leaves the list empty. Handles a special case for SpatioTemporal tables: when rowOrCol is "column", the categories are populated with GeoCount empty strings. These empty placeholders represent geographic locations filled in at runtime. The resulting BetterArray is cloned before returning to prevent external mutations from affecting internal state.

Parameters:

Returns: BetterArray. A cloned array containing the category strings.

Depends on:


CategoriesOf #

categories-of

Ask an object for the categories of one variable

Signature:

Private Function CategoriesOf(ByVal lData As Object, _
                              ByVal nameVar As String) As BetterArray

Asks lData for the categories of nameVar and answers what it gives back. The parameter is typed Object so that no caller has to hold a LinelistSpecs, and asking is the only way to learn whether the object supplies categories: error 438 says the member is absent and answers Nothing. Every other error belongs to the object itself and is raised on, so a fault inside a real LinelistSpecs.Categories still reaches the caller.

Parameters:

Returns: BetterArray. The categories, or Nothing when lData has no Categories member.


ComputeTableScope #

compute-table-scope

Compute the AnalysisTableScope from the ListObject name

Signature:

Private Function ComputeTableScope() As AnalysisTableScope

Reads the name of the ListObject the specification row sits in and maps it to the matching AnalysisTableScope constant. Called once during InitCache to populate the scope cache. The seven names are fixed by the setup template and the same in the mock, in the shipped file and in every release build.

Returns: AnalysisTableScope. The table scope category.

Throws:


ComputeValidTable #

compute-valid-table

Compute whether this table has a valid configuration

Signature:

Private Function ComputeValidTable() As Boolean

Validates whether this table specification row contains all required fields and references valid variables for its analysis type. Called once on first access of ValidTable to populate the Boolean cache. Each branch resolves only the variables its own rule needs.

The same rules run on both sides. The designer asks after LLdictionary.Prepare, where a geo variable has become adm1_; the setup error checks ask before it, where the variable still carries its own name and its "geo" or "hf" control. The two spatial branches read the control on an unprepared dictionary and the prefixed name on a prepared one, so the setup stops calling a spatial row invalid without letting a row through that the analysis build would then fail to draw.

Returns: Boolean. True if the table specification meets all requirements.

Throws:

Depends on:


PreviousValidSpec #

previous-valid-spec

The nearest row above this one that the build will draw

Signature:

Private Function PreviousValidSpec() As TableSpecs

Walks backward one row at a time from the row above this one and answers the first candidate whose ValidTable is True, whatever section it belongs to. The walk stops at the header row, to stay inside the data area, and the answer is held for the life of the instance.

This is the one search behind both IsNewSection and Previous, and it has to stay free of both: ComputeValidTable reads the scope, the cached values, the dictionary and the variable reader, and touches neither of them, so the walk cannot recurse.

Returns: TableSpecs. The nearest valid row above this one, or Nothing when there is none between here and the header.


ComputeIsNewSection #

compute-is-new-section

Compute whether this table begins a new section group

Signature:

Private Function ComputeIsNewSection() As Boolean

Compares this row's section value against the section value of the nearest row above it that the build will actually draw. A row whose section differs from that one starts a section, and so does a row with no valid row above it at all. Called once on first access of IsNewSection to populate the Boolean cache.

The comparison used to read the row physically above, whatever state it was in. Everything else in the pipeline works over valid tables only, and that mismatch is what broke a section whose anchor row failed validation: the anchor was skipped, every row below it still reported False, and each one appended itself to section infrastructure nothing had built. It broke the mirror case too, where an invalid row carrying a new section name hid the boundary from the valid row under it.

Sections are matched without regard to case, because the section column is free text: a capitalisation slip would otherwise split one section in two and every section-keyed named range would follow it.

Returns: Boolean. True if this table starts a new section.


Error Handling

ThrowError #

throw-error

Raise a ProjectError-based exception

Signature:

Private Sub ThrowError(ByVal errNumber As Long, ByVal message As String)

Logging and error-raising helpers.

Wrapper around Err.Raise that standardises the source to CLASS_NAME, providing a consistent stack trace across all methods in this class.

Parameters:

Throws:


CheckRequirements #

check-requirements

Validate preconditions for the Create factory

Signature:

Private Sub CheckRequirements(ByVal headerRng As Range, _
                              ByVal tableSpecsRng As Range, _
                              ByVal dict As LLdictionary)

Validates all preconditions required by the Create factory before populating a new TableSpecs instance. Checks that headerRng, tableSpecsRng, and dict are not Nothing, that both ranges span a single row, that they start at the same column, that they have the same column count, and that the header sits above the specification row.

The column-start check matters because ComputeIsNewSection resolves the section index against the header range and then indexes the specification range with it: two ranges starting at different columns make every section read land on the wrong column.

Parameters:

Throws:


Used in (19 file(s))