LLLog
Keeps the dated record of the important user actions of a running linelist on the very hidden __log worksheet. Each event is one Checking bundle: the bundle title is the SECTION the action belongs to, the entry label carries the timestamp, the action code and a short detail, and the scope carries the outcome. Every bundle is flushed through CheckingOutput.PrintOutput, which appends across renders, so the sheet grows one line per event and the built-in status and title dropdowns of the output sheet filter the log by outcome and by section.
WHAT ONE LINE SAYS
An entry line reads as the timestamp and the platform, then the procedure that raised the event, then the action code and the detail:
2026-08-15 13:55:45 mac-64 excel-16.112 ClickShowHideSection > showhide-section: Demographics hidden on sheet1
The procedure is named by the caller. VBA carries no call stack to read a name from, and one button can raise the same refusal from four places, so a line that names only the action leaves the reader guessing which press wrote it. The name is optional: a caller that passes none writes the same line as before, opening at the action code.
THE THREE SECTIONS
The log is read by three sections, and the title of an entry is the section the action belongs to. The writer runs in compact mode, so a section title is written the first time the sheet carries it and never again: a session of a hundred actions reads as a hundred lines under at most three titles.
open/close the workbook open, the workbook close, a log restart data input/output every import, every export, and clearing the data linelist lifecycle everything else the user does inside the workbook
An action code no case names lands in linelist lifecycle, so a new logged action reaches the sheet without touching this class. The action code itself moves down to the entry line, beside the timestamp, where it stays readable without heading a block of its own.
A title heads a block of its own kind. An action coming back to a section written earlier lands at the foot of THAT block, not at the foot of the sheet, so the three blocks stay whole however far apart in time their actions were taken, and a blank row stands between them. Inside a block the lines keep the order they happened. Each line also carries its section in the hidden title column, and that is what the title dropdown of the output sheet filters on, so reading the log one section at a time is a dropdown away.
The last thing that happened is therefore no longer the last line of the sheet: a close lands under the open it belongs with, near the top, while the lifecycle block below carries the bulk of the session.
PROVISIONING
Create(wkb) binds to the __log worksheet of the given workbook and builds it very hidden when the workbook has none. An old linelist grows the sheet on its first logged event, so the generation tree stays untouched. Building a sheet wants the workbook structure unprotected, which is the caller''s duty.
ROTATION
LOG_MAX_ENTRIES is read as a row cap. A flush that finds the last written row past the cap clears the sheet and removes the row marker CheckingOutput keeps, so the next render re-initialises the sheet, and the fresh log opens with one info line ("restarted").
THE STOPWATCH OF A LONG WALK
StartWalk, MarkStep, StepReport and LogSteps time a walk that runs in steps -- an export, an import -- and leave the whole walk on one info line, each step with its seconds and a total at the end. MarkStep answers the name it was given, so a walk that already carries the step it is on keeps one line per step and the name still reaches its failure message. The stopwatch is state on the instance, so one instance times one walk. One line rather than one per step: a migration export names a dozen steps, and a line each would push the row cap twelve times faster for the same reading.
FILTER DROPDOWNS
The interactive row hiding of the output sheet wants a Worksheet_Change handler injected through the VBE, and injecting code wants VBE trust, which field machines lack. LLLog leaves the worksheet code module alone; the dropdowns stay plain validation cells and the filtering is reachable through CheckingOutput.FilterWorksheet.
Version: 1.2 (2026-08-31)
Factory
Create #
create
Create a log store bound to the __log worksheet of a workbook
Signature:
Public Function Create(ByVal wkb As Workbook) As LLLog
Entry point for creating LLLog instances.
Binds to the __log worksheet of the given workbook. When the workbook has none, the sheet is appended after the last worksheet and made very hidden, so an old linelist grows its log on first use.
Parameters:
wkb: Workbook. The linelist workbook carrying the log.
Returns: LLLog. Fully initialised instance.
Throws:
- ProjectError.ObjectNotInitialized When wkb is Nothing.
PublicAccessors
SheetName #
sheet-name
Name of the log worksheet
Signature:
Public Property Get SheetName() As String
Answers the internal sheet name so the viewing surface and the close guard reach the sheet through the class.
Returns: String. The log worksheet name.
SectionOf #
section-of
The section an action code is logged under
Signature:
Public Function SectionOf(ByVal action As String) As String
The three sections are the whole title vocabulary of the log sheet. The workbook open, the workbook close and a log restart are the session boundaries and read as open/close; everything that moves data in or out of the workbook reads as data input/output; every other action the user takes inside the workbook reads as linelist lifecycle, which is also where an action code named by no case below lands.
Parameters:
action: String. The action code of the event.
Returns: String. One of the three section titles.
MaxEntries #
max-entries
Row cap of the log sheet
Signature:
Public Property Get MaxEntries() As Long
Answers the cap the rotation reads, so a test can seed a row past it without writing ten thousand lines.
Returns: Long. The row cap.
Logging
LogSuccess #
log-success
Log an action that ended well
Signature:
Public Sub LogSuccess(ByVal action As String, _
Optional ByVal detail As String = vbNullString, _
Optional ByVal source As String = vbNullString)
One method per outcome, each flushing one Checking bundle.
Parameters:
action: String. The action code, used as the bundle title.detail: Optional String. Short free text beside the timestamp.source: Optional String. The procedure that raised the event.
LogWarning #
log-warning
Log a refusal or a degraded run
Signature:
Public Sub LogWarning(ByVal action As String, _
Optional ByVal detail As String = vbNullString, _
Optional ByVal source As String = vbNullString)
Parameters:
action: String. The action code, used as the bundle title.detail: Optional String. Short free text beside the timestamp.source: Optional String. The procedure that raised the event.
LogFailure #
log-failure
Log an action that failed
Signature:
Public Sub LogFailure(ByVal action As String, _
Optional ByVal detail As String = vbNullString, _
Optional ByVal source As String = vbNullString)
Parameters:
action: String. The action code, used as the bundle title.detail: Optional String. Short free text beside the timestamp.source: Optional String. The procedure that raised the event.
LogInfo #
log-info
Log a lifecycle line
Signature:
Public Sub LogInfo(ByVal action As String, _
Optional ByVal detail As String = vbNullString, _
Optional ByVal source As String = vbNullString)
Parameters:
action: String. The action code, used as the bundle title.detail: Optional String. Short free text beside the timestamp.source: Optional String. The procedure that raised the event.
StepTiming
StartWalk #
start-walk
Open a fresh stopwatch on this log
Signature:
Public Sub StartWalk()
The stopwatch of a long walk, and the one line it leaves behind.
Called at the top of a long walk -- an export, an import -- before the first step is named. Anything an earlier walk left behind is dropped, so a walk that failed before it wrote its line never leaks its steps into the next one. The stopwatch is state on the instance, so one instance times one walk. LLExporter holds its own log and LinelistRun reads the one the event service holds, which is why the export walk and the import walk never meet in here.
MarkStep #
mark-step
Close the step that was running and open the one named here
Signature:
Public Function MarkStep(ByVal stepName As String) As String
Answers the name it was given, so a walk already carrying the step it is on in a variable keeps one line per step:
atStep = logStore.MarkStep("reading the dictionary")
The name still reaches the failure message, and the stopwatch rides along. A walk with no stopwatch open is left alone and the name comes straight back, so a caller whose workbook takes no log writes the same code.
Parameters:
stepName: String. What the walk is about to do.
Returns: String. The name it was given.
StepReport #
step-report
The whole walk on one line, each step with its seconds
Signature:
Public Function StepReport() As String
Reads the pairs and adds the total, and reads them without touching them: a step still running is reported with the seconds it has spent so far and stays open. So this answers the same walk whether it is called at the end or at a failure label. The seconds are written through Str, not Format, because Format follows the decimal separator of the machine and this box writes a comma. A timing line is read beside the timing line of another machine, so the two have to be written the same way.
Returns: String. The line, empty when no walk is open.
LogSteps #
log-steps
Write the walk''s step times as one info line, and close the walk
Signature:
Public Sub LogSteps(ByVal action As String, _
Optional ByVal source As String = vbNullString)
One line per walk rather than one per step: a migration export names a dozen steps, and a line each would push the row cap of the sheet twelve times faster for the same reading. The action code is the walk''s own, so the timing line lands in the same section block as the success or failure line it belongs with. A walk with no stopwatch open writes nothing, so a caller can call this on every path.
Parameters:
action: String. The action code of the walk.source: Optional String. The procedure that ran the walk.
Export
ReportLines #
report-lines
The whole log as plain text, one line per element
Signature:
Public Function ReportLines() As BetterArray
Writing the log out as a file a user can send on.
The text a user sends on when something goes wrong, so it stands on its own: the workbook it came from and the platform it is written on, then every row of the Metadata worksheet, then the log itself in the order it was written. The log SHEET is the record here, unlike GenerationLog which keeps its run in memory, so the rows are read back off the sheet. The text is built here rather than inside ExportText so what a file will say can be read without a file being written. A log that cannot reach its metadata still answers its entries.
Returns: BetterArray. The lines, lower bound 1.
Throws:
- ProjectError.ObjectNotInitialized When the log has no worksheet.
ExportText #
export-text
Write the whole log out as a plain text file
Signature:
Public Function ExportText(ByVal folderPath As String, _
Optional ByVal baseName As String = vbNullString) As String
The file lands in the given folder as
Parameters:
folderPath: String. The folder taking the file.baseName: String. The file name without its suffix. The workbook name when empty.
Returns: String. The full path of the written file.
Throws:
- ProjectError.ObjectNotInitialized When the log has no worksheet.
- ProjectError.InvalidArgument When no folder is given.
Internal members (not exported)
PublicAccessors
Wksh #
wksh
Retrieve the log worksheet backing this store
Signature:
Public Property Get Wksh() As Worksheet
Properties that expose internal state.
Returns: Worksheet. Reference to the log worksheet.
Wksh #
wksh-set
Define the worksheet that will receive the log
Signature:
Public Property Set Wksh(ByVal sh As Worksheet)
Parameters:
sh: Worksheet. Target worksheet to use.
Internals
ResolveLogSheet #
resolve-log-sheet
Resolve the log worksheet, building it very hidden when absent
Signature:
Private Function ResolveLogSheet(ByVal wkb As Workbook) As Worksheet
Sheet resolution, entry building, and rotation.
The sheet is appended after the last worksheet, because a bare Add puts a new sheet in front of whatever is active. An existing sheet keeps its visibility, so the viewing surface can hold it open across a bind.
Parameters:
wkb: Workbook. The linelist workbook.
Returns: Worksheet. The resolved or newly created worksheet.
AppendHeadingTo #
write-heading-to
The first block of the report: which workbook, and on what
Signature:
Private Sub AppendHeadingTo(ByVal lines As BetterArray, ByVal sh As Worksheet)
The platform of the export and the platform of an entry can differ, since a linelist travels between machines, and both are worth having.
Parameters:
lines: BetterArray. The lines built so far.sh: Worksheet. The log worksheet.
AppendMetadataTo #
append-metadata-to
Every row of the Metadata worksheet, into the report
Signature:
Private Sub AppendMetadataTo(ByVal lines As BetterArray, ByVal wkb As Workbook)
The whole variable and value block goes out, not a chosen few rows, so a linelist built by a later designer carries whatever that designer wrote without this routine being touched again. The block below the creation rows is the geobase metadata, and it goes out with the rest. A linelist with no Metadata worksheet says so and the report goes on: a log is worth having even when the metadata is missing.
Parameters:
lines: BetterArray. The lines built so far.wkb: Workbook. The linelist workbook.
AppendEntriesTo #
append-entries-to
Every row of the log sheet, into the report
Signature:
Private Sub AppendEntriesTo(ByVal lines As BetterArray, ByVal sh As Worksheet)
A row carrying the title alone opens a section and reads as a heading in brackets; every other written row is one entry, and it reads as the timestamp, the outcome in brackets, then the action and the detail. Blank rows are dropped, because CheckingOutput lays them out for the eye and they say nothing.
Parameters:
lines: BetterArray. The lines built so far.sh: Worksheet. The log worksheet.
PlainOutcome #
plain-outcome
The outcome word of a row, without the picture in front of it
Signature:
Private Function PlainOutcome(ByVal text As String) As String
Checking paints an outcome with a symbol before the word -- a cross for an error, a tick for a success, an i in a circle for an info line -- and that symbol is what the sheet shows. The text export cannot carry it. Print # writes one byte per character, so every one of those symbols reaches the file as a single stray mark, and a log a user sends on reads "[_ Error]" and "[_ Info]" throughout. The word alone is what the file wants. A section title carries no symbol and comes back untouched: only a first character outside plain text is taken off.
Parameters:
text: String. The cell value, already trimmed.
Returns: String. The word alone.
CellText #
cell-text
One cell as text, with an error value reading as empty
Signature:
Private Function CellText(ByVal cellValue As Variant) As String
A cell holding #N/A or #REF! raises 13 on CStr, and one such cell in a log or on a metadata sheet must not take down the export of everything else.
Parameters:
cellValue: Variant. The value read from a cell.
Returns: String. The value as text, or an empty string when it is an error.
WriteEntry #
write-entry
Rotate when the cap is passed, then flush one dated entry
Signature:
Private Sub WriteEntry(ByVal action As String, ByVal detail As String, _
ByVal scope As Byte, _
Optional ByVal source As String = vbNullString)
Parameters:
action: String. The action code, used as the bundle title.detail: String. Short free text beside the timestamp.scope: Byte. CheckingScope value carrying the outcome.source: String. The procedure that raised the event. May be empty.
Throws:
- ProjectError.InvalidArgument When the action code is empty.
FlushEntry #
flush-entry
Build one Checking bundle and flush it through the writer
Signature:
Private Sub FlushEntry(ByVal action As String, ByVal detail As String, _
ByVal scope As Byte, _
Optional ByVal source As String = vbNullString)
The bundle title is the section of the action, not the action itself, so the sheet carries three titles rather than one per action code. The bundle carries one key, so the key uniqueness of Checking binds inside the bundle and every event stays a plain append.
Parameters:
action: String. The action code, written on the entry line.detail: String. Short free text beside the timestamp.scope: Byte. CheckingScope value carrying the outcome.source: String. The procedure that raised the event. May be empty.
EntryLabel #
entry-label
Compose the dated label of one entry
Signature:
Private Function EntryLabel(ByVal action As String, ByVal detail As String, _
Optional ByVal source As String = vbNullString) As String
The label opens with the timestamp and the platform; the procedure, the action code and the detail follow behind the separator, so the two halves land in their own columns of the log sheet. The action code moved here when the title became the section: the sheet has three titles now, and the line has to say which action it records. A separator inside the free text would push it past the last written column, so it is softened to a single dash. The procedure sits in front of the action because that is the order a reader wants: who acted, what they did, and on what. The platform rides with the timestamp rather than with the detail, because the detail column is what a reader searches and what several tests read whole. It is on every line on purpose: a log rotation clears the sheet, so a platform written once at the top of a session would not survive one.
Parameters:
action: String. The action code of the event.detail: String. Short free text beside the timestamp.source: String. The procedure that raised the event. May be empty.
Returns: String. The composed label.
Soften #
soften
Take the column separator out of a piece of free text
Signature:
Private Function Soften(ByVal text As String) As String
CheckingOutput splits the label on the separator and lays the pieces across three columns, so a separator inside free text would push the rest of the line past the last written column and out of the sheet.
Parameters:
text: String. The free text.
Returns: String. The text with every separator turned into a single dash.
PlatformTag #
platform-tag
The operating system, the bitness and the Excel version, on one tag
Signature:
Private Function PlatformTag() As String
A log read months later says nothing about where it was written, and the same code does not behave the same on the two platforms: a geobase export that Windows accepts has refused on this Mac. Every entry therefore names its platform. The name and the bitness come from the compile constants, so they are the build that is running and cannot be misread. The Excel version comes from the application and is read once, because it is the same for every line of a session and the log writes a line per user action.
Returns: String. Something of the shape "mac-64 excel-16.90".
RotateWhenPastCap #
rotate-when-past-cap
Clear the sheet and restart the log once the cap is passed
Signature:
Private Sub RotateWhenPastCap()
Reads the last written row of the output column. Past the cap, the sheet is cleared and the row marker removed, so CheckingOutput re-initialises on the next render, and the fresh log opens with one info line.
Writer #
writer
Lazily create and return the CheckingOutput writer
Signature:
Private Function Writer() As CheckingOutput
The writer is asked for compact appending, which is the mode a log wants: a section title is written the first time the sheet carries it and never again, and no blank rows stand between the entries.
Returns: CheckingOutput. Writer bound to the log worksheet.
HiddenStore #
hidden-store
Lazily create and return the HiddenNames store of the log sheet
Signature:
Private Function HiddenStore() As HiddenNames
Returns: HiddenNames. Hidden name store bound to the log worksheet.
CloseOpenStep #
close-open-step
Keep the step that was running, with the seconds it took
Signature:
Private Sub CloseOpenStep()
EndWalk #
end-walk
Close the stopwatch and drop what it held
Signature:
Private Sub EndWalk()
TimerNow #
timer-now
The stopwatch reading, as a Double
Signature:
Private Function TimerNow() As Double
Timer answers a Single, which carries about seven digits: past midday the seconds since midnight already use six of them, so a Single reads hundredths at best. The widening happens here, once, so every subtraction below is done in Double.
Returns: Double. Seconds since midnight.
ElapsedBetween #
elapsed-between
The seconds between two stopwatch readings
Signature:
Private Function ElapsedBetween(ByVal fromAt As Double, _
ByVal toAt As Double) As Double
A walk that runs across midnight reads as a negative difference, because Timer restarts at zero. A day is added back on.
Parameters:
fromAt: Double. The earlier reading.toAt: Double. The later reading.
Returns: Double. The seconds between them.
SecondsText #
seconds-text
A number of seconds as text, the same way on every machine
Signature:
Private Function SecondsText(ByVal seconds As Double) As String
Str is used rather than Format because Format follows the decimal separator of the machine: this box is en_FR and writes 2,15 where a Windows box in English writes 2.15. Two timing lines that are read side by side have to be written the same way. Str drops the leading zero of a value under one, so it is put back.
Parameters:
seconds: Double. The seconds to write.
Returns: String. Something of the shape "2.15s".
ErrorHandling
ThrowError #
throw-error
Raise a ProjectError-coded runtime error
Signature:
Private Sub ThrowError(ByVal errNumb As Long, ByVal errorMessage As String)
Centralised error-raising pattern and the set-once seal.
Parameters:
errNumb: Long. ProjectError enumeration value.errorMessage: String. Human-readable description.
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.
Used in (16 file(s))
- FilteredData.cls
- LLExporter.cls
- EventLinelist.cls
- Linelist.cls
- MasterSetupLog.cls
- EventsLinelistButtons.bas
- LinelistRun.bas
- FormLogicAdvanced.bas
- FormLogicExportMig.bas
- TestFilteredData.bas
- TestLLExporterTiming.bas
- TestLLImportTiming.bas
- TestGeoModule.bas
- TestEventLinelist.bas
- TestLLLog.bas
- TestShowHideTiming.bas