Formulas

Reads a setup expression and answers the Excel formula it stands for. The expression is split into tokens, the variable names are resolved through an LLdictionary, and the custom functions are handed to CaseWhen, ChoiceFormula and ValueOfFormula. SetupFormula takes the expression in, Valid says whether it can be read, Reason names what stopped it, Varlists lists the variables it names and HasSetupVariables says whether it names any. ParsedLinelistFormula answers the formula for a data entry sheet, and ParsedAnalysisFormula the one for an analysis sheet.

GROUPED FORMULAS

IsGrouped says whether the expression uses one of the grouped custom functions. Such an expression is written out as the Excel aggregation FormulaData maps the token to.

WHAT IS REPORTED

Entries filed while an expression is read leave through HasChecking and CheckingValues.

Depends on: BetterArray, Checking, LLdictionary, LLVariables, LLSheets, CaseWhen, ChoiceFormula, ValueOfFormula

Version: 1.0 (2026-02-09)

Factory

Create #

create

Instantiate a Formulas helper ready to parse the provided setup expression

Signature:

Public Function Create(ByVal dict As LLdictionary, _
                       ByVal formData As FormulaData, _
                       ByVal setupForm As String) As Formulas

Provides the single entry point to instantiate the parser and initialise core state.

Validates that the required dictionary and formula data objects are present, then stores them in a new instance alongside the setup expression. No eager parsing occurs; the formula is parsed lazily on first access to Valid, Varlists, or any parsed formula property.

Parameters:

  • dict: LLdictionary. Variable metadata provider.
  • formData: FormulaData. Catalogue of approved Excel functions and separators.
  • setupForm: String. Pseudo-formula to parse and validate.

Returns: Formulas. Initialised instance.

Throws:

  • ProjectError.ObjectNotInitialized When dict is Nothing.
  • ProjectError.ObjectNotInitialized When formData is Nothing.

PublicAccessors

HasSetupVariables #

has-setup-variables

Determine if the parsed formula contains literal values

Signature:

Public Property Get HasSetupVariables() As Boolean

Returns True when numeric or text literals were encountered during tokenisation. The flag is set during EvaluateFormula.

Returns: Boolean. True when literals are present.


IsGrouped #

is-grouped

Indicate whether the parsed formula targets grouped evaluation

Signature:

Public Property Get IsGrouped() As String

Forces evaluation in the linelist context, then inspects the captured group metadata. Returns "Yes" when grouped logic applies, "No" otherwise.

Returns: String. "Yes" for grouped formulas, "No" otherwise.


Reason #

reason

Retrieve the last validation message associated with the given context

Signature:

Public Property Get Reason(Optional ByVal formulaType As String = CONTEXT_ANALYSIS) As String

Returns a human-readable explanation of the current validity state. When the formula is valid the default success message is returned; otherwise the recorded invalidation reason is returned. EnsureEvaluation answers with the validity flag, so one test decides which of the two messages comes back.

Parameters:

  • formulaType: Optional String. Context (analysis/linelist/simple).

Returns: String. Explanation of the current validity state.


Valid #

valid

Check whether the formula is valid for the supplied context

Signature:

Public Property Get Valid(Optional ByVal formulaType As String = CONTEXT_ANALYSIS) As Boolean

Triggers a lazy evaluation for the requested context, then returns the cached validity flag.

Parameters:

  • formulaType: Optional String. Context (analysis/linelist/simple).

Returns: Boolean. True when parsing succeeded.


Varlists #

varlists

Provide a clone of the variables detected in the expression

Signature:

Public Property Get Varlists(Optional ByVal formulaType As String = CONTEXT_ANALYSIS) As BetterArray

Returns a BetterArray containing the variable names encountered during tokenisation. The result is a clone so callers cannot mutate the internal cache.

Parameters:

  • formulaType: Optional String. Context (analysis/linelist/simple).

Returns: BetterArray. Variable names.


ParsedLinelistFormula #

parsed-linelist-formula

Build the Excel expression for linelist worksheets

Signature:

Public Property Get ParsedLinelistFormula(Optional ByVal useTableName As Boolean = False, _
                                          Optional ByVal tablePrefix As String = vbNullString) As String

Walks the cached token list and writes the output string as it goes, replacing variable tokens with worksheet addresses or structured references depending on the useTableName flag. Grouped formulas are delegated to BuildGroupedFormula. Custom tokens MEAN and N are mapped to their Excel equivalents (AVERAGE, COUNT). The token list is read where it lies. CrossTableFormula reaches one instance from 47 call sites, and each of those used to copy the whole list.

Parameters:

  • useTableName: Optional Boolean. Toggles structured references.
  • tablePrefix: Optional String. Prefixed to structured references.

Returns: String. Excel formula referencing linelist variables.


ParsedAnalysisFormula #

parsed-analysis-formula

Build the Excel expression for aggregated analysis results

Signature:

Public Property Get ParsedAnalysisFormula(ByVal formCond As FormulaCondition, _
                                          Optional ByVal tablePrefix As String = vbNullString, _
                                          Optional ByVal Connector As String = "*") As String

Walks the cached token list and writes the output string as it goes, replacing variable tokens with conditional aggregation fragments supplied by formCond. Custom formula tokens are delegated to ParsedCustomFormula. Grouped formulas are delegated to BuildGroupedFormula. A custom token that expands to a whole call swallows the "(" and ")" tokens behind it, which is what the token removal used to do by shortening the list while the loop ran.

Parameters:

  • formCond: FormulaCondition. Provides conditional aggregation logic.
  • tablePrefix: Optional String. Prefix for table references.
  • Connector: Optional String. Connector between conditional fragments.

Returns: String. Excel expression ready for analysis worksheets.


Checkings

HasChecking #

has-checkings

Indicate whether any diagnostic entries were recorded

Signature:

Public Property Get HasChecking() As Boolean

Returns the flag set by LogCheck. VarWriter reads this name at four call sites, so the spelling stays until a session owns that file.

Returns: Boolean. True when checkings exist.


CheckingValues #

checking-values

Expose the collected checking entries if available

Signature:

Public Property Get CheckingValues() As Checking

Returns the internal Checking store when at least one entry has been recorded. Returns Nothing otherwise.

Returns: Checking. Logged diagnostics.


Internal members (not exported)

Factory

Seal #

seal

Prevent further changes to setup-only properties

Signature:

Public Sub Seal()

Marks the instance as sealed so guarded setters raise when invoked after construction. Called by the factory before returning. The class holds a parse result, and a setter running behind it would leave tokens that belong to another expression.


GuardNotSealed #

guard-not-sealed

Reject writes to setup-only properties after sealing

Signature:

Private Sub GuardNotSealed(ByVal propName As String)

Raises an error when a guarded setter is invoked on a sealed instance.

Parameters:


PublicAccessors

Dictionary #

dictionary

Retrieve the underlying dictionary used during parsing

Signature:

Public Property Get Dictionary() As LLdictionary

Exposes configured dependencies, cached evaluation results, and parsed Excel expressions.

Returns the LLdictionary previously supplied during Create or Initialise. Used by downstream helpers to resolve variable metadata.

Returns: LLdictionary. Dictionary backing metadata lookups.


Dictionary #

dictionary-set

Assign a new dictionary and reset dependent caches

Signature:

Public Property Set Dictionary(ByVal dict As LLdictionary)

Replaces the current dictionary and invalidates all lazily resolved dependencies. The factory seals the instance, so this raises after construction.

Parameters:

Throws:


Data #

data

Retrieve the formula configuration catalogue

Signature:

Public Property Get Data() As FormulaData

Returns the FormulaData describing available Excel tokens, operators, and special characters used during tokenisation.

Returns: FormulaData. Configuration catalogue.


Data #

data-set

Update the formula configuration catalogue and invalidate caches

Signature:

Public Property Set Data(ByVal formData As FormulaData)

Replaces the current formula data and invalidates all cached parsing results. The factory seals the instance, so this raises after construction.

Parameters:

Throws:


SetupFormula #

setup-formula

Retrieve the original setup pseudo-formula

Signature:

Public Property Get SetupFormula() As String

Returns the raw expression as captured from the setup sheet during Create or the last assignment.

Returns: String. Raw expression.


SetupFormula #

setup-formula-set

Store the setup pseudo-formula and reset computed caches

Signature:

Public Property Let SetupFormula(ByVal setupForm As String)

Replaces the stored expression and invalidates all cached parsing results. The factory seals the instance, so this raises after construction.

Parameters:

Throws:


PrivateHelpers

Initialise #

initialise

Store incoming dependencies and prepare caches

Signature:

Public Sub Initialise(ByVal dict As LLdictionary, _
                       ByVal formData As FormulaData, _
                       ByVal setupForm As String)

Internal utilities for dependency management, cache invalidation, and token resolution.

Assigns the dictionary, formula data, and setup expression, then drops the lazily built helpers and invalidates the parse cache once. It stays Public because the factory calls it on a second instance, and it is guarded: after the factory seals that instance, this raises.

Parameters:

Throws:


IsCustomFormula #

is-custom-formula

Determine whether a token is one of the custom analysis formulas

Signature:

Private Function IsCustomFormula(ByVal token As String) As Boolean

MEAN, N and N() are the whole set and they are compiled in. The match is case-sensitive, which is what the old list lookup did. AppendToken accepts these in the analysis context only, and ParsedAnalysisFormula tests the same three. Both tests are needed.

Parameters:

Returns: Boolean. True when the token is a custom analysis formula.


InvalidateCaches #

invalidate-caches

Reset cached parsing results and computed flags

Signature:

Private Sub InvalidateCaches()

Clears the token list, variable list, validity flags, and group context so the next access triggers a full reparse from the stored setup expression.


ResetGroupContext #

reset-group-context

Clear any grouped-formula metadata captured during previous evaluations

Signature:

Private Sub ResetGroupContext()

Resets all fields of the TGroupContext UDT to their default values.


VariablesProvider #

variables-provider

Lazily instantiate the LLVariables helper from the dictionary

Signature:

Private Function VariablesProvider() As LLVariables

Creates the LLVariables wrapper on first access and caches it for subsequent calls.

Returns: LLVariables. Bound to the current dictionary.


SheetProvider #

sheet-provider

Lazily instantiate the LLSheets helper for address resolution

Signature:

Private Function SheetProvider() As LLSheets

Creates the LLSheets wrapper on first access and caches it for subsequent calls.

Returns: LLSheets. Bound to the current dictionary.


EvaluationContext #

evaluation-context

Map the three context names onto the two real contexts

Signature:

Private Function EvaluationContext(ByVal formulaType As String) As String

"analysis" is one context and everything else is the other. Production passes "linelist", the class passes itself "simple", and the public defaults are "analysis". Every branch of EvaluateFormula asks the same question, so holding three names made the same expression parse twice for the same answer.

Parameters:

Returns: String. CONTEXT_ANALYSIS or CONTEXT_LINELIST.


EnsureEvaluation #

ensure-evaluation

Guarantee that a parsing/validation run exists for the requested context

Signature:

Private Function EnsureEvaluation(ByVal formulaType As String) As Boolean

Checks whether the cached evaluation matches the requested context. When a mismatch is detected or no evaluation has been performed, a full EvaluateFormula pass is triggered.

Parameters:

Returns: Boolean. True when the cached evaluation is valid.


EvaluateFormula #

evaluate-formula

Parse the setup expression, populate tokens, and capture validation results

Signature:

Private Sub EvaluateFormula(ByVal context As String)

Converts the stored setup formula through custom function handlers, then tokenises the result while validating parentheses and literals. Short-circuits for single-variable or single-custom-function expressions. Delegates grouped formula detection to GroupContexExtractionSucceed.

Parameters:


GroupFormula

GroupContexExtractionSucceed #

group-context-extraction

Analyse the tokenised expression and record grouped metadata when relevant

Signature:

Private Function GroupContexExtractionSucceed(ByVal tokens As BetterArray, _
                                       ByVal variableTokens As BetterArray, _
                                       ByVal vars As LLVariables, _
                                       ByVal dataSource As FormulaData) As Boolean

Detects grouped formulas, validates arguments, and prepares metadata for downstream builders.

Inspects the root function name to determine whether the formula targets grouped evaluation. When grouped, validates that exactly three variable arguments are present and that the first and third variables belong to the same table. Records the captured metadata in the TGroupContext UDT for BuildGroupedFormula.

Parameters:

Returns: Boolean. True when grouped context is captured or not required.


RootFunctionName #

root-function-name

Retrieve the entry token corresponding to the root function

Signature:

Private Function RootFunctionName(ByVal tokens As BetterArray) As String

Returns the first token from the tokenised expression, which is expected to be the function name when the formula is function-based.

Parameters:

Returns: String. Root token or empty string when unavailable.


IsGenericGroupFunction #

is-generic-group-function

Determine whether a function uses the GROUP prefix for grouped evaluation

Signature:

Private Function IsGenericGroupFunction(ByVal functionName As String) As Boolean

Returns True when the token starts with "GROUP" (case-insensitive). A token such as GROUPING_X is read as generic and its aggregator fails the whitelist check with a clear message, which is the wanted behaviour.

Parameters:

Returns: Boolean. True when the token starts with "GROUP".


ExtractGenericAggregator #

extract-generic-aggregator

Parse the aggregator portion from a GROUP-prefixed function

Signature:

Private Function ExtractGenericAggregator(ByVal functionName As String) As String

Splits the function name at the underscore separator and returns the trailing portion as the aggregator token. When no underscore is present, strips the "GROUP" prefix directly.

Parameters:

Returns: String. Aggregator token to apply when building the Excel formula.


BuildGroupedFormula #

build-grouped-formula

Construct the grouped Excel expression using captured metadata

Signature:

Private Function BuildGroupedFormula(ByVal useTableName As Boolean, _
                                     ByVal tablePrefix As String) As String

Uses the TGroupContext UDT to resolve variable ranges and emit the appropriate aggregation expression. Native *IFS functions produce direct SUMIFS/COUNTIFS calls; non-native functions wrap the result in an IF-based array formula.

Parameters:

Returns: String. Grouped Excel formula ready for consumption.


GroupedDataRange #

grouped-data-range

Resolve the appropriate range reference for a grouped variable

Signature:

Private Function GroupedDataRange(ByVal variableName As String, _
                                  ByVal useTableName As Boolean, _
                                  ByVal tablePrefix As String, _
                                  ByVal sheets As LLSheets) As String

Emits either a structured table reference or a sheet-level address depending on the useTableName flag.

Parameters:

Returns: String. Excel reference targeting the grouped variable.


Tokenisation

TokeniseFormula #

tokenise-formula

Break the converted formula into tokens while validating parentheses and literals

Signature:

Private Function TokeniseFormula(ByVal formula As String, _
                                 ByVal context As String, _
                                 ByVal tokens As BetterArray, _
                                 ByVal variableTokens As BetterArray, _
                                 ByRef HasSetupVariables As Boolean, _
                                 ByRef failureReason As String) As Boolean

Breaks expressions into tokens, validates each chunk, and handles custom formula expansion.

Walks the formula character by character, splitting on special characters defined by the FormulaData configuration. Tracks parenthesis depth and quotation state. Each extracted chunk is validated through AppendToken. The walk reads one character at a time. CaseWhen, ChoiceFormula and ValueOfFormula hold the same shape of walk over their own bodies. What made this loop expensive was the three Excel calls inside CleanString and the array copy inside the character lookup, and both of those are gone.

Parameters:

Returns: Boolean. True when parsing succeeds.


AppendToken #

append-token

Validate and append the provided chunk to the tokens collection

Signature:

Private Function AppendToken(ByVal chunk As String, _
                             ByRef context As String, _
                             ByRef tokens As BetterArray, _
                             ByRef variableTokens As BetterArray, _
                             ByRef vars As LLVariables, _
                             ByRef dataSource As FormulaData, _
                             ByRef HasSetupVariables As Boolean, _
                             ByRef failureReason As String) As Boolean

Classifies the chunk as a grouped function, known variable, boolean literal, registered Excel formula, custom formula, numeric literal, or quoted string. Unknown chunks cause a validation failure. The grouped-function test runs on the FIRST token only. That is what stops SUM(SUMIFS(...)) from being read as a grouped formula.

Parameters:

Returns: Boolean. True when the chunk is accepted.


IsQuotedString #

is-quoted-string

Determine whether the supplied value is wrapped in quotes

Signature:

Private Function IsQuotedString(ByVal value As String) As Boolean

Returns True when the string starts and ends with a double-quote character and has a length of at least two.

Parameters:

Returns: Boolean. True when the string is quoted.


IsNumericLiteral #

is-numeric-literal

Determine whether the supplied chunk is an English-form numeric literal

Signature:

Private Function IsNumericLiteral(ByVal chunk As String) As Boolean

Accepts digits around at most one dot. The convention keeps setup formulas in English, and Range.Formula reads US syntax on every host, so the English form is the one form a literal may take here. IsNumeric would read the chunk in the HOST's regional separators, and on a comma machine it refused every dot literal a setup file carried. A sign never reaches this test: '+' and '-' are separator characters, so they split off before the chunk arrives.

Parameters:

Returns: Boolean. True when the chunk holds at least one digit and nothing besides digits and a single dot.


CleanString #

clean-string

Normalise the incoming token by removing control characters and duplicate spaces

Signature:

Private Function CleanString(ByVal value As String) As String

Replaces non-breaking spaces, drops control characters, squeezes runs of spaces to one, and trims the ends. This is what the worksheet SUBSTITUTE, CLEAN and TRIM trio did, written in plain VBA: the three of them crossed into Excel once each, and this runs once per token of every formula in the workbook. AscW answers a negative number above code point 32767, so the control-code test guards on zero. Worksheet TRIM squeezes runs of spaces INSIDE the text as well as at the ends, so that is reproduced.

Parameters:

Returns: String. Trimmed and cleaned representation.


ParsedCustomFormula #

parsed-custom-formula

Delegate parsing to custom functions when present in analysis context

Signature:

Private Function ParsedCustomFormula(ByVal customFunction As String, _
                                     ByVal formCond As FormulaCondition, _
                                     Optional ByVal tablePrefix As String = vbNullString) As String

Handles MEAN (mapped to AVERAGE), N/N() (mapped to COUNTIFS), and passes through unknown custom identifiers unchanged. The COUNTIFS expansion builds criteria pairs from the FormulaCondition. The table always comes from the FormulaCondition. It used to be carried from whichever variable token came first, so the same expression gave two answers depending on the order of its tokens.

Parameters:

Returns: String. Excel fragment produced by the custom handler.


EmptyInvocationFollows #

empty-invocation-follows

Report whether the two tokens after a custom formula are an empty ()

Signature:

Private Function EmptyInvocationFollows(ByVal functionIndex As Long) As Boolean

When a custom formula token is replaced inline, the "(" and ")" tokens behind it become redundant and are skipped.

Parameters:

Returns: Boolean. True when the next two tokens are "(" then ")".


ClearCountIf #

clear-count-if

Remove equality fragments from COUNTIF-style conditions

Signature:

Private Function ClearCountIf(ByVal value As String) As String

Sanitises predicate fragments by stripping equality operators and normalising less-than and greater-than comparisons for COUNTIFS compatibility. FormulaCondition builds the fragments this rewrites, so the two have to keep agreeing on their shape.

Parameters:

Returns: String. Sanitised predicate suitable for COUNTIFS.


ConvertedSetupFormula #

converted-setup-formula

Convert higher-level setup directives into plain Excel expressions

Signature:

Private Function ConvertedSetupFormula() As String

Detects CASE_WHEN, CHOICE_FORMULA, and VALUE_OF prefixes in the stored setup expression and delegates to the corresponding parser class. Returns the original expression when no prefix matches. The expression is trimmed first. The three prefix tests anchor at position 1, so a leading space sent the raw CASE_WHEN text to the tokeniser, which reported it as an unknown token. When a converter answers with an empty string, its own failure message is held so the caller can report what really went wrong.

Returns: String. Converted formula ready for tokenising.


SetInvalid #

set-invalid

Record an invalidation reason and log the associated checking entry

Signature:

Private Sub SetInvalid(ByVal message As String, Optional ByVal scope As Byte = checkingError)

Stores the failure message, marks the formula as invalid, and logs the message through the internal Checking store.

Parameters:


Checkings

LogCheck #

log-check

Record a diagnostic entry within the internal checking store

Signature:

Private Sub LogCheck(ByVal label As String, _
                     Optional ByVal scope As Byte = checkingNote)

Aggregates informational and error traces emitted during parsing.

Lazily creates the Checking instance on first use, then appends the labelled entry with the given severity scope.

Parameters:


ErrorHandling

ThrowError #

throw-error

Raise a VBA error with the class signature

Signature:

Private Sub ThrowError(ByVal errNumb As Long, ByVal errorMessage As String)

Centralises the error raising pattern for consistent ProjectError usage.

Wrapper around Err.Raise that standardises the source to the class constant CLASSNAME for consistent stack traces. The message goes through as written, the same as the other five classes in this folder.

Parameters:

Throws:


Used in (21 file(s))