Developer Reference Manual | Version 1.0 | March 2026
The Shared Library is a collection of nine .bas module files that contain
battle-tested VBA utility functions extracted from real production macros in this
repository. Every function has been used in at least one working project.
Because VBA cannot share compiled code between workbooks at runtime (there is no DLL or add-in mechanism in this setup), the library is a copy-paste reference: you import the modules you need into your new project, and they become a permanent part of that workbook. Changes you make in one workbook do not automatically propagate to others — that is intentional.
NzStr, a new Levenshtein, or a new date
formatter from scratch on every project, you import the tested version from here and move
straight to business logic.
Application.ScreenUpdating /
EnableEvents / DisplayAlerts / Calculation
pattern always lives in the project's own Orchestrator module. See CLAUDE.md §9.SharedLibrary\ and select the .bas file(s) you
need. Import one file per operation.Attribute VB_Name will be the name shown (e.g.
LIB1_StringUtils).FindHeaderColMultiRow (which calls NzStr from LIB1) will produce
a compile error.
.bas files directly onto the VBE window — works
in Excel 2016 and later.
The LIB prefix is registered to the Shared Library folder itself.
When you import a module into a real project (e.g. BPA), you have two options:
LIB1_StringUtils, etc.
BPA1_StringUtils, and update the
Attribute VB_Name line at the top of the file. Also update every
internal call from LIB1. to BPA1. if VBA ever needs to
qualify the module name (it usually does not, because all public functions are visible
project-wide without a module qualifier).
| Module | Primary purpose | Depends on | Required by |
|---|---|---|---|
| LIB1 LIB1_StringUtils.bas | String cleaning, normalisation, path utilities | none | LIB2, LIB4 |
| LIB2 LIB2_SheetUtils.bas | Sheet safety, header finding, Dictionary helpers | LIB1 (for FindHeaderColMultiRow) | LIB5 |
| LIB3 LIB3_DateUtils.bas | Date formatting and safe parsing | none | — |
| LIB4 LIB4_FuzzyMatch.bas | Levenshtein distance and vendor name scoring | none | — |
| LIB5 LIB5_Logging.bas | Safe log sheet append (3-col and 9-col models) | LIB2 (GetOrCreateSheet) | — |
| LIB6 LIB6_ProgressUI.bas | Progress bar wrapper skeleton | requires frmProgress UserForm |
— |
| LIB7 LIB7_NumericUtils.bas | Account number normalisation, price conversion | none | — |
| LIB8 LIB8_HtmlUtils.bas | HTML encoding, email body/address utilities | none | — |
| LIB9 LIB9_OrgUtils.bas | Oracle OU code extraction and matching | none | — |
Safe string conversion, Unicode-aware text cleaning, accent normalisation, path/filename helpers, and splitting. No dependencies. Used by almost every other module in the library.
Returns "" for Null, Error values, or anything that can't be converted with
CStr(). Use everywhere you read from a cell — it prevents the
"Type mismatch" and "Invalid use of Null" runtime errors that occur when
a cell contains an Excel error formula result (#N/A, #VALUE!,
etc.) or a database Null.
Dim cellVal As String cellVal = NzStr(ws.Cells(i, colAmount).Value) ' safe even for #N/A cells
Full Unicode-aware sanitiser. Removes all ASCII control characters (via Excel's
CLEAN()), normalises every Unicode space variant (NBSP, thin space, em space,
etc.) to a regular space, strips zero-width characters and BOM, then collapses runs of
multiple spaces and trims. Use this before any lookup, comparison, or code substitution.
Dim vendor As String vendor = GF_StrictClean(CStr(ws.Cells(i, colVendor).Value)) If vendor = "" Then GoTo NextRow
Lighter version of the above. Does NOT remove control characters (so bullet characters and other printable non-ASCII characters survive). Use for email body text or any field where you want to preserve special characters but still fix invisible-space paste artifacts.
Calls GF_StrictClean then removes any remaining Chr(160) (NBSP).
Drop-in replacement for the old manual CleanText helper used across earlier
projects. Identical to GF_StrictClean for practical purposes.
Keeps only A–Z, a–z, 0–9, and space. Use when a config flag like
Strip_Special_Chars = YES is set, or when normalising a code field before
exact comparison.
Dim code As String
code = StripNonAlphanumeric("ACC-0012 / AU") ' -> "ACC0012 AU" then collapsed -> "ACC0012 AU"
Prepares a person or company name for fuzzy comparison. Lower-cases, strips honourifics
(Mr., Mrs., Dr., Prof., etc.), replaces all punctuation with spaces, collapses spaces.
Use on both sides before calling Similarity() or ScorePair().
Dim a As String, b As String
a = NormalizeForFuzzy("Spafford, Ms. Torie") ' -> "spafford torie"
b = NormalizeForFuzzy("TORIE SPAFFORD") ' -> "torie spafford"
Lower-cases and maps accented / diacritic characters to their ASCII equivalents. Covers standard Latin accents (à→a, é→e, ü→u, ñ→n, etc.) and Polish diacritics (ą→a, ć→c, ę→e, ł→l, etc.). Use for fuzzy name matching across inputs from different locales and source systems.
Dim n As String
n = NormalizeText("Müller") ' -> "muller"
n = NormalizeText("Łukasz") ' -> "lukasz"
For column-agnostic header matching. Trims, collapses spaces, normalises all Unicode dash
variants (en-dash, em-dash, hyphen, etc.) to a plain hyphen, lower-cases. Use together
with GetColByHeader() in LIB2 to survive Excel files where headers were pasted
from Word and contain typographic dashes.
' "E–mail" (en-dash) and "E-mail" (hyphen) both become "e-mail" Dim h As String h = NormalizeHeaderText(ws.Cells(1, c).Value)
Appends piece to note with "; " as separator. Returns
piece if note is currently empty (so you never get a leading
"; "). Use for building audit notes and log action strings.
Dim note As String note = ConcatNote(note, "Price normalised") note = ConcatNote(note, "Lead time clamped to upper bound") ' -> "Price normalised; Lead time clamped to upper bound"
Removes one layer of matching double-quotes from both ends of a string. Use when reading CSV-style values that were pasted with surrounding quotes.
Dim v As String
v = StripOuterQuotes("""hello""") ' -> "hello"
v = StripOuterQuotes("hello") ' -> "hello" (no change, no outer quotes)
Splits by comma and trims each resulting token. Safer than a raw
Split(s, ",") call because it handles leading/trailing spaces around commas.
Dim parts() As String
parts = SplitCommaSafe(" AU_611 , US_151 , GB_201 ")
' parts(0) = "AU_611", parts(1) = "US_151", parts(2) = "GB_201"
Joins two path segments with Application.PathSeparator. Handles the case
where a already ends with a separator, so you never get double backslashes.
Dim fullPath As String
fullPath = PathJoin(ws.Range("B2").Value, ws.Range("B3").Value)
' -> "C:\Output\Files\report.xlsx" even if B2 ended with "\"
Makes a string safe for use as a Windows filename token. Collapses spaces to underscores,
removes all illegal Windows filename characters (\ / : * ? " < > |),
strips leading/trailing underscores.
Dim token As String
token = SanitizeSegment("Hello World!") ' -> "Hello_World"
token = SanitizeSegment("Q1 / AU Review") ' -> "Q1_AU_Review"
Dim fname As String
fname = FormatDateForFilename(Date) & "_" & token & ".xlsx"
' -> "26_MAR_2026_Q1_AU_Review.xlsx"
Worksheet safety wrappers (no more Runtime Error 9), header-column finding at any row, auto-detection of the header row in messy files, column management, folder creation, formula-to-value conversion, and Dictionary helpers. Depends on LIB1.
Returns the named sheet, creating it at the end of the workbook if it does not yet exist. Never raises Runtime Error 9. Use this for output sheets that must exist before you write to them.
Dim wsLog As Worksheet Set wsLog = GetOrCreateSheet(ThisWorkbook, "Log") wsLog.Cells(1, 1).Value = "Ready"
Returns the named sheet, or Nothing if it does not exist. Never crashes.
Use this instead of Worksheets("name") whenever the sheet's existence is
not guaranteed. Always test If ws Is Nothing Then after calling it.
Dim wsOptional As Worksheet
Set wsOptional = GetSheetIfExists(ThisWorkbook, "Config")
If wsOptional Is Nothing Then
ShowError "Config sheet missing"
GoTo CleanUp
End If
Returns True if the named sheet exists in the workbook.
If Not SheetExists(ThisWorkbook, "Archive") Then
MsgBox "Run SetupWorkbook first to create the Archive sheet."
Exit Sub
End If
Deletes all ListObjects (Excel Tables) on the sheet first, then clears all cells. Use at the start of a run to reset an output sheet without leaving orphaned table objects that would cause errors when you re-write headers.
ClearSheetAndTables wsTodo wsTodo.Cells(1, 1).Value = "Line Number" ' safe to write headers now
Case-insensitive match in row 1 using Application.Match. Returns the
1-based column index, or 0 if not found. Fastest option when the header is always in row 1.
Dim colVendor As Long
colVendor = FindHeaderCol(ws, "Vendor Name")
If colVendor = 0 Then
MsgBox "Column 'Vendor Name' not found"
Exit Sub
End If
Case-insensitive scan in a specific row number. More reliable than
Application.Match when the header contains extra spaces. Use when you already
know which row the header is in but want to handle leading/trailing spaces.
Dim colEmail As Long colEmail = FindHeaderInRow(ws, "Email", 3) ' header is in row 3
Scans rows 1 through maxRow (default 3) for an exact case-insensitive match. Use when source files are inconsistent — sometimes the header is in row 1, sometimes row 2 (because of a title row above it).
Dim colSupplier As Long colSupplier = FindHeaderColMultiRow(ws, "Supplier Number", 5)
Use these three functions together when you receive files where the header row position can vary and you cannot hardcode it:
Scans rows startRow through endRow and returns the first row where at least 3 of the supplied column names are found. Returns 0 if no qualifying row is found.
Returns a Scripting.Dictionary mapping normalised header text to its
1-based column number. All keys are processed through NormalizeHeaderText()
so Unicode dashes, extra spaces, and case differences do not matter.
Tries each name in the names array against the map dictionary. Returns the column of the first match, or 0. Use synonym arrays to handle alternative spellings in one call.
' Full three-step pattern for variable-format files:
Dim hdrRow As Long
hdrRow = DetectHeaderRow(ws, Array("OU", "Supplier Num", "Email"), 1, 5)
If hdrRow = 0 Then MsgBox "Cannot find header row": Exit Sub
Dim hdrMap As Object
Set hdrMap = MapHeaderColumns(ws, hdrRow)
Dim colOU As Long : colOU = GetColByHeader(hdrMap, Array("OU", "Org"))
Dim colEmail As Long : colEmail = GetColByHeader(hdrMap, Array("E-mail", "Email", "Email Address"))
Dim colSupNum As Long : colSupNum = GetColByHeader(hdrMap, Array("Supplier Num", "Supplier Number", "Vendor ID"))
Looks for an existing column with that header in row 1 and returns it. If not found, appends the header at the first empty column and returns that new column number. Use to add output columns (BUYER_ID, SOURCING_RULE, STATUS) to a copy of the source data.
Dim colStatus As Long colStatus = GetOrAddOutputCol(wsTodo, "Processing Status") wsTodo.Cells(todoRow, colStatus).Value = "DONE"
Applies bold + background colour + white font to a range. Default colour is Excel blue
(RGB(68, 114, 196)). Pass any RGB() value as the second argument
to use a different colour.
StyleHeader ws.Rows(1) ' default blue
StyleHeader ws.Range("A1:K1"), RGB(0, 176, 240) ' custom cyan
Creates a folder path including all intermediate parent folders using
FileSystemObject. Works with UNC paths and deep nesting. Returns
True on success, False if an error occurs (e.g. no write
permission).
Dim outFolder As String
outFolder = PathJoin(ws.Range("B5").Value, "Output\2026-03")
If Not EnsureFolderExists(outFolder) Then
MsgBox "Cannot create output folder: " & outFolder
GoTo CleanUp
End If
Converts every formula cell in ws.UsedRange to its displayed string value.
Uses cell.Text (not cell.Value) so that leading zeros are
preserved — for example a cell formatted as "00000" containing
=A1 will become the string "00048" rather than the number
48. Call this before SaveAs or any column scan on files that
may contain formula-driven account numbers.
ConvertFormulasToValues dataWS ' call once on the source sheet after opening
Creates a new Scripting.Dictionary. Default is case-insensitive
(vbTextCompare = 1). Pass False to get a case-sensitive
dictionary.
Dim cache As Object
Set cache = NewDict() ' case-insensitive (default)
Set cache = NewDict(False) ' case-sensitive
cache("VENDOR A") = 42
cache("vendor a") = 42 ' same key in case-insensitive mode
Safe key-existence check that handles Nothing without crashing. Equivalent to
d.Exists(k) but safe to call even if the dictionary was never initialised.
Linear search for a string value in a Collection.
Joins all items in a Collection with a delimiter string. Equivalent to
Join(arr, delim) but works on Collection objects.
Locale-insensitive date formatting for filenames, cells, email subjects, and report titles. Safe date parsing that never crashes on empty or badly-formatted inputs. No dependencies.
| Function | Output format | Example output | Typical use |
|---|---|---|---|
FormatDateForFilename(dt) |
DD_MMM_YYYY (upper) | 26_MAR_2026 |
Output filename prefix |
FormatDateForCell(dt) |
DD-MMM-YY (upper) | 26-MAR-26 |
Writing a run date into a cell |
FormatDateLong(dt) |
DD-MMM-YYYY (upper) | 26-MAR-2026 |
Email subject, archive name |
FormatDateFromVariant(v) |
DD-MMM-YYYY (upper) | 26-MAR-2026 |
Reading a cell that might be empty |
Format$() which is locale-aware for the month
abbreviation separator but the month name itself comes from the Windows locale.
On English-locale machines this produces MAR, JAN, etc. as
expected.
' Building a dated output filename: Dim fname As String fname = FormatDateForFilename(Date) & "_" & SanitizeSegment(projectName) & ".xlsx" ' -> "26_MAR_2026_Q1_AU_BPA.xlsx"
Parses dateStr and checks that the result is today or in the future. Writes the parsed date back through the outDate ByRef parameter on success. Use to validate a run-date entered by the user in a cell or TextBox.
Dim runDate As Date
If Not ValidateRunDate(ws.Range("B1").Value, runDate) Then
MsgBox "Run date must be today or a future date."
Exit Sub
End If
Returns True and sets d on success; returns False on
any failure. Never raises an error. Use when you want explicit success/fail branching.
Dim d As Date
If TryParseDate(CStr(ws.Cells(i, colDate).Value), d) Then
ws.Cells(i, colOutput).Value = FormatDateLong(d)
Else
ws.Cells(i, colOutput).Value = "INVALID DATE"
End If
Returns the parsed date, or 0 (= #30 Dec 1899#) on any failure.
Use when you want a simple return value rather than a ByRef out-parameter.
SafeDate is an alias for TryCDate.
Dim d As Date
d = TryCDate(CStr(ws.Cells(i, colDate).Value))
If d = 0 Then
SafeLog ThisWorkbook, "DateParser", "Row " & i & " — unparseable date"
End If
Levenshtein edit distance, similarity scoring, token-sort comparison, and company-name preprocessing. Used for matching vendor names from different source systems where spelling, punctuation, and word order differ. No dependencies.
Returns the minimum number of single-character edits (insert, delete, substitute) needed
to transform s into t. Dynamic-programming O(n×m) implementation.
Rarely called directly — use Similarity() or ScorePair() instead.
Converts Levenshtein distance to a 0.0–1.0 similarity score. 1.0 = identical, 0.0 = no
characters in common. Use this for simple two-string comparisons; use
ScorePair() for the full vendor-name matching pipeline.
If Similarity("ACME CORP", "ACME CORPORATION") > 0.8 Then
' close enough
End If
Prepares a vendor or company name for comparison: upper-cases, strips punctuation (periods, commas, dashes, ampersands, brackets, slashes), removes noise words as whole words (LLC, INC, LTD, THE, CORP, CO). Always apply to both sides before scoring.
CleanCompanyName("Smith, LLC.") ' -> "SMITH"
CleanCompanyName("The Widget Co.") ' -> "WIDGET"
CleanCompanyName("ACME & Partners") ' -> "ACME PARTNERS"
Sorts the space-separated tokens of a cleaned name alphabetically. This means "WIDGET BLUE ABC" and "ABC BLUE WIDGET" produce the same key, so word order differences (common when the same vendor is entered differently in different systems) do not reduce the similarity score.
The main entry point for vendor name matching. Combines three strategies and returns the maximum:
Pre-compute CleanCompanyName and TokenSortKey once for each side
before entering the inner loop to avoid O(n²) redundant work.
' Pre-compute once for the query name: Dim aClean As String : aClean = CleanCompanyName(queryVendor) Dim aTok As String : aTok = TokenSortKey(aClean) ' Inner loop over candidates: Const ACCEPT_THRESHOLD As Double = 0.72 Dim i As Long For i = 2 To lastRow Dim bClean As String : bClean = CleanCompanyName(CStr(oracleWS.Cells(i, colName).Value)) Dim bTok As String : bTok = TokenSortKey(bClean) Dim score As Double : score = ScorePair(aClean, aTok, bClean, bTok) If score >= ACCEPT_THRESHOLD Then ' Found a match — record it and exit matchName = CStr(oracleWS.Cells(i, colName).Value) Exit For End If Next i
In-place ascending quicksort on a string array slice. Used internally by
TokenSortKey. Can also be used directly on any string array you need sorted.
Two complementary logging models in one module. Both create the Log sheet automatically on first call and never crash if the sheet is absent. Depends on LIB2.
Appends one row to the Log sheet with three columns: Timestamp, Where
(caller label), Message. Best for event-level tracing: recording that something happened,
that a file was opened, that a match was not found.
SafeLog ThisWorkbook, "FuzzyMatch", "No match found for vendor: " & vendorName SafeLog ThisWorkbook, "BuildRegistry", "Registry loaded: " & entryCount & " entries"
Appends one row to the Log sheet with nine columns: Timestamp, Macro,
Source File, Source Sheet, Source Row, Index, Decision, Outcome, Actions/Notes.
Best for row-by-row processing records where you need a full audit trail of which rows
were accepted, skipped, or fixed and why.
| Parameter | Column | What to pass |
|---|---|---|
wb | — | ThisWorkbook |
macroName | B | Name of the Sub/Function logging this row |
sourceFile | C | Filename of the source workbook |
sourceSheet | D | Sheet name in the source workbook |
sourceRow | E | Row number in the source data (0 for summary events) |
indexVal | F | The row's unique index value (e.g. LP number, BPA index) |
decisionVal | G | The decision column value from that row (or "") |
outcome | H | One of: START, DONE, SKIPPED, FIXED_AND_PROCESSED, ABORTED, INFO, WARN, ERROR |
actions | I | Human-readable description of what happened and why |
' Summary event (no specific row): GF_LogRowAction ThisWorkbook, "BuildToDoSheet", "", "", 0, "", "", _ "START", "Log cleared for new run" ' Row skipped: GF_LogRowAction ThisWorkbook, "BuildToDoSheet", srcFile, srcTab, i, idxVal, _ "", "SKIPPED", "Duplicate of source row 14 (KEEP_FIRST mode)" ' Row fixed and kept: GF_LogRowAction ThisWorkbook, "BuildToDoSheet", srcFile, srcTab, i, idxVal, _ "", "FIXED_AND_PROCESSED", "PRICE: [1.234,56] -> [1234.56] (EU format converted)"
Ensures the Log sheet exists with 9-column headers. Returns the worksheet.
Called internally by GF_LogRowAction; you rarely need to call it directly
unless you want to pre-create the sheet before the first action row.
Clears all data rows (A2 downward) while keeping the header row intact. Call at the start of a new run so the log only contains entries for the current session.
' Always clear at the start of a run:
GF_ClearLogSheet ThisWorkbook
GF_LogRowAction ThisWorkbook, "MyMacro", "", "", 0, "", "", "START", "New run started"
SafeLog for simple macros with no
row loop, or for logging events outside the main loop (file opened, config loaded).
Use GF_LogRowAction in any macro that processes rows one by one and needs
a full audit trail reviewers can filter in Excel.
A thin wrapper module that connects your macro's main loop to a progress bar UserForm.
The wrapper isolates all form references so the macro compiles and runs cleanly even when
the form is not present (useful during testing). Requires an frmProgress
UserForm in the same workbook.
LIB6_ProgressUI.bas into the project.frmProgress
that exposes these members:
Sub Init(titleText As String, totalItems As Long)Sub UpdateProgress(processed As Long)Sub SetTitle(s As String)Sub ConfigureTimeGate(enable As Boolean, ms As Long, min As Long)Sub BeginMarquee(msg As String)Sub EndMarquee()Property Get IsCancelled() As BooleanfrmBPAProgress), update the two
lines marked ' *** rename here in the module.
Shows the form, calls Init, sets a 200 ms time gate (so fast loops don't
flicker), and yields to Windows with DoEvents. Call once before the main loop.
Updates the bar value and calls DoEvents. The DoEvents here is
critical — it is what prevents Excel from going "Not Responding" during long loops
and what allows the Cancel button click to be processed. Call once per loop iteration.
Returns True if the user clicked the Cancel button on the form. Check this
inside your loop to stop gracefully.
Switches the bar to indeterminate (spinning) mode for phases where you don't know the
total item count (e.g. sorting, saving). Call ProgressEndMarquee when that
phase ends to return to normal mode.
Hides and unloads the form, clears the status bar. Call from the CleanUp:
label so it always runs, even if the macro aborted early.
' Typical main-loop pattern: ProgressStart "Processing rows...", lastRow - 1 Dim i As Long For i = 2 To lastRow If ProgressCancelled() Then SafeLog ThisWorkbook, "MyMacro", "User cancelled at row " & i GoTo CleanUp End If ProgressUpdate i - 1 ' ... process row i ... Next i ProgressMarquee "Saving output file..." ' ... save file ... ProgressEndMarquee CleanUp: ProgressFinish
Digit extraction, account-number normalisation (scientific notation, leading zeros), and price format conversion from EU comma-decimal to US dot-decimal with salvage logic. No dependencies.
Returns only the digit characters (0–9) from a string. All other characters are removed. Leading zeros are preserved because the result is a string, not a number.
OnlyDigits("ACC-0012345 ") ' -> "0012345"
OnlyDigits("AU_611_GDSAU_OU") ' -> "611"
Returns True if s is non-empty and consists entirely of digits 0–9.
Masks an account number to its last 4 digits for safe display in notes, logs, or emails. Strings shorter than 4 digits are returned unchanged.
TailMask("1234567890") ' -> "...7890"
TailMask("AB12") ' -> "AB12" (too short to mask, returned as-is)
Expands an Excel scientific-notation string (e.g. 1.53697E+11) to its full
plain digit string (153697000000) without using VBA's numeric types, which
would lose precision. Returns "" if no E character is found.
Uses VBScript.RegExp for robust detection of scientific notation; returns the
expanded digit string. Non-scientific strings are returned unchanged. Use when reading
account numbers from cells that Excel may have auto-formatted as scientific notation.
NormalizeAccountStr("1.53697E+11") ' -> "153697000000"
NormalizeAccountStr("ACC001234") ' -> "ACC001234" (unchanged)
Full normalisation pipeline: handles scientific notation, strips all non-digit characters,
removes trailing .00 decimal portions. Use to normalise both sides before
exact-match comparison of account numbers from different source systems.
CanonicalizeAccountStr("ACC-001234.00") ' -> "001234"
CanonicalizeAccountStr("1.23E+8") ' -> "123000000"
CanonicalizeAccountStr(" 007 500 ") ' -> "007500"
Returns True if the value looks like it needs conversion. Checks for: letter
or currency symbol characters (€, $, £, ¥), multiple dots, multiple commas, any comma,
or any space within a numeric-looking value.
If GF_PriceNeedsNormalization(ws.Cells(i, colPrice).Value) Then
' warn user or auto-fix
End If
Strips currency symbols, converts EU comma-decimal format to US dot-decimal, forces
exactly 2 decimal places. Returns a formatted string like "1234.56".
Sets wasChanged = True if any transformation was applied.
Sets note to a plain-English description of what changed.
Returns "" if the value is empty or cannot be parsed after cleanup.
Salvage logic (controlled by module-level constants):
"13.218.22" → "13218.22" (multi-dot: last segment is
decimals, earlier dots are thousands separators)"13,218,22" → "13218.22" (multi-comma: same logic)
Dim priceNorm As String
Dim wasChanged As Boolean, priceNote As String
priceNorm = GF_NormalizePriceToUS(ws.Cells(i, colPrice).Value, wasChanged, priceNote)
If Len(priceNorm) = 0 Then
GF_LogRowAction ThisWorkbook, "MyMacro", srcFile, srcTab, i, idxVal, _
"", "SKIPPED", "Unparseable price: [" & CStr(ws.Cells(i, colPrice).Value) & "]"
GoTo NextRow
End If
ws.Cells(i, colPrice).NumberFormat = "@"
ws.Cells(i, colPrice).Value = priceNorm
If wasChanged Then SafeLog ThisWorkbook, "PriceFix", "Row " & i & ": " & priceNote
Locale-safe "0.00" formatting. Always outputs a dot as decimal separator and
no thousands separator, regardless of Windows regional settings. Use when writing a price
to a cell or file that will be processed by another system.
Returns True for "YES" or "Y" (case-insensitive).
Use to parse a decision/flag column where the user types Yes, YES, Y, or yes.
HTML email composition utilities: entity encoding, plain-text-to-HTML conversion with bullet detection, email address validation and normalisation, attachment path building, and ON/OFF flag parsing. No dependencies.
Escapes the five HTML special characters so they display literally in a browser or Outlook:
& → &, < → <,
> → >, " → ",
' → '.
HtmlEncode("AT&T <sender>") ' -> "AT&T <sender>"
Simple conversion: encodes entities and converts line breaks to <br>.
Use for signature blocks or plain paragraphs where you do not need bullet list detection.
Full rich conversion:
<br><br> (paragraph break)•, -, or *
→ <li> items inside <ul><br>Dim body As String body = "Dear Vendor," & vbCrLf & vbCrLf body = body & "Please review your account details:" & vbCrLf body = body & "- Bank: ANZ" & vbCrLf body = body & "- Account: ...7890" & vbCrLf Dim htmlBody As String htmlBody = PlainToHtmlRich(body) mail.HTMLBody = WrapHtml(htmlBody)
Collapses 3 or more consecutive blank lines to 1. Call on the body string before passing
it to PlainToHtmlRich to avoid excessive vertical whitespace when users paste
from Word.
Replaces the placeholder [[BANK_ACCOUNTS]] in baseText with
accountsHtml, converting the rest of the body to rich HTML. If the placeholder is
absent, the accounts block is appended at the end. Use for templated vendor notification
emails where the account list is injected at a specific position in the body.
Dim tmplBody As String
tmplBody = CStr(ws.Range("B5").Value) ' "Please review [[BANK_ACCOUNTS]] above."
Dim acctHtml As String
acctHtml = BuildAccountsHtml(acctList)
mail.HTMLBody = WrapHtml(ComposeBodyHtml(tmplBody, acctHtml))
Converts a comma- or newline-separated list of account numbers to an HTML bullet list
(<ul>). Returns <em>Not currently on file.</em>
for empty input.
Wraps a body fragment in a complete HTML document with charset UTF-8 and
Calibri/Arial 11pt body font. Assign the result directly to mail.HTMLBody.
Basic structural check: verifies @ exists with at least one character before
it, a dot exists after the @, and the dot is not the last character. Not a
full RFC validator — fast enough for filtering obviously bad addresses.
Splits a semicolon-or-comma-separated list of addresses, validates each with
IsValidEmail, and returns the valid ones joined with semicolons (Outlook format).
Returns "" if no valid addresses are found.
mail.To = NormalizeEmails(ws.Cells(i, colEmail).Value)
If Len(mail.To) = 0 Then
SafeLog ThisWorkbook, "SendEmail", "Row " & i & " — no valid To address, skipped"
GoTo NextRow
End If
Builds a full attachment file path from a folder and filename. Returns "" if
either part is empty. Handles the case where the folder already ends with a separator.
Interprets ON/OFF/YES/NO/TRUE/FALSE/1/0 strings as Boolean. Returns defaultVal
(default: True) for any unrecognised input (including blank). Use to read
toggle settings from cells or configuration tables.
Dim sendAttach As Boolean
sendAttach = ParseOnOff(ws.Range("B8").Value, False) ' default off if blank
Oracle Operating Unit (OU) code extraction and symmetric matching. Handles the three ORG formats that appear in Oracle ERP: full string, short code, and digits only. No dependencies.
| Format name | Example | Description |
|---|---|---|
| Full | AU_611_GDSAU_OU | Complete ORG string from Oracle |
| Short | AU_611 | Country code + OU number |
| Digits | 611 | Numeric OU code only |
Returns "AU_611" from "AU_611_GDSAU_OU". Returns ""
if the string does not match the expected pattern (2-char country code + underscore +
3-digit OU).
Returns "611" from "AU_611_GDSAU_OU". Returns ""
if the string does not match.
Returns True if sourceOrg and userOrg refer to the same
Operating Unit, regardless of which format either side uses. The matching is
symmetric — either argument can be in any format.
A blank userOrg is treated as a wildcard and always returns True.
| sourceOrg | userOrg | Result |
|---|---|---|
AU_611_GDSAU_OU | 611 | True |
AU_611 | AU_611_GDSAU_OU | True |
611 | AU_611 | True |
AU_611 | US_151_DSUSA_OU | False |
AU_611 | "" (blank) | True (wildcard) |
' Filtering Oracle rows by user-selected OU:
Dim userOU As String
userOU = Trim$(CStr(wsRun.Range("K6").Value)) ' "" means "all OUs"
Dim i As Long
For i = 2 To lastRow
Dim rowOrg As String
rowOrg = GF_StrictClean(CStr(oracleWS.Cells(i, colOrg).Value))
If Not OrgMatches(rowOrg, userOU) Then GoTo NextRow
' ... process row i ...
NextRow:
Next i
These end-to-end examples show how the library modules work together in realistic situations drawn from the macros in this repository.
You receive an Oracle extract every month, but the columns are in a different order each time and sometimes there is a title row above the headers. You need to find the columns you care about reliably, regardless of layout.
Modules needed: LIB1, LIB2
' Step 1: auto-detect the header row anywhere in rows 1-5 Dim hdrRow As Long hdrRow = DetectHeaderRow(dataWS, Array("Supplier Num", "Email", "OU"), 1, 5) If hdrRow = 0 Then MsgBox "Cannot identify header row in source file." GoTo CleanUp End If ' Step 2: map all headers to column numbers Dim hdrMap As Object Set hdrMap = MapHeaderColumns(dataWS, hdrRow) ' Step 3: look up each column with synonym lists Dim colSupplier As Long Dim colEmail As Long Dim colOU As Long Dim colVendName As Long colSupplier = GetColByHeader(hdrMap, Array("Supplier Num", "Supplier Number", "Vendor ID")) colEmail = GetColByHeader(hdrMap, Array("E-mail", "Email", "Email Address", "Contact Email")) colOU = GetColByHeader(hdrMap, Array("OU", "Org", "Operating Unit")) colVendName = GetColByHeader(hdrMap, Array("Vendor Name", "Supplier Name", "Name")) ' Step 4: process rows (hdrRow+1 onward) Dim lastRow As Long lastRow = dataWS.UsedRange.Row + dataWS.UsedRange.Rows.Count - 1 Dim i As Long For i = hdrRow + 1 To lastRow Dim supplierNum As String supplierNum = GF_StrictClean(NzStr(dataWS.Cells(i, colSupplier).Value)) If Len(supplierNum) = 0 Then GoTo NextRow Dim email As String email = NormalizeEmails(NzStr(dataWS.Cells(i, colEmail).Value)) ' ... process row ... NextRow: Next i
You have a list of vendors from a bank payment file and a list from Oracle. The names don't match exactly — one system uses "ACME CORP", the other "Acme Corporation Pty Ltd". You need to find the best match from Oracle for each bank vendor.
Modules needed: LIB1, LIB4, LIB5
Const ACCEPT_THRESHOLD As Double = 0.72 ' Pre-load Oracle names into an array once: Dim oracleCount As Long oracleCount = oracleWS.Cells(oracleWS.Rows.Count, colOracleName).End(xlUp).Row - 1 Dim oracleClean() As String, oracleTok() As String ReDim oracleClean(1 To oracleCount) ReDim oracleTok(1 To oracleCount) Dim j As Long For j = 1 To oracleCount oracleClean(j) = CleanCompanyName(NzStr(oracleWS.Cells(j + 1, colOracleName).Value)) oracleTok(j) = TokenSortKey(oracleClean(j)) Next j ' Match each bank row: Dim i As Long For i = 2 To lastBankRow Dim bankName As String bankName = GF_StrictClean(NzStr(bankWS.Cells(i, colBankName).Value)) Dim aClean As String : aClean = CleanCompanyName(bankName) Dim aTok As String : aTok = TokenSortKey(aClean) Dim bestScore As Double : bestScore = 0 Dim bestIdx As Long : bestIdx = 0 For j = 1 To oracleCount Dim score As Double score = ScorePair(aClean, aTok, oracleClean(j), oracleTok(j)) If score > bestScore Then bestScore = score bestIdx = j End If Next j If bestScore >= ACCEPT_THRESHOLD Then bankWS.Cells(i, colMatchName).Value = NzStr(oracleWS.Cells(bestIdx + 1, colOracleName).Value) bankWS.Cells(i, colMatchScore).Value = Format$(bestScore, "0.00") bankWS.Cells(i, colMatchStatus).Value = "MATCHED" Else bankWS.Cells(i, colMatchStatus).Value = "NO MATCH" SafeLog ThisWorkbook, "FuzzyMatch", "No match for: " & bankName & _ " (best=" & Format$(bestScore, "0.00") & ")" End If Next i
Your macro generates an output Excel file, saves it to a timestamped folder, and shows a progress bar while processing rows. The user should be able to cancel.
Modules needed: LIB1, LIB2, LIB3, LIB5, LIB6
Public Sub RunGenerateReport()
On Error GoTo CleanUp
' 1. Build output folder and filename
Dim outFolder As String
outFolder = PathJoin(ws.Range("B5").Value, FormatDateForFilename(Date))
If Not EnsureFolderExists(outFolder) Then
MsgBox "Cannot create output folder: " & outFolder
GoTo CleanUp
End If
Dim outFile As String
outFile = PathJoin(outFolder, _
FormatDateForFilename(Date) & "_" & SanitizeSegment(ws.Range("B2").Value) & ".xlsx")
' 2. Prepare output sheet
Dim wsOut As Worksheet
Set wsOut = GetOrCreateSheet(ThisWorkbook, "Output")
ClearSheetAndTables wsOut
wsOut.Cells(1, 1).Value = "Vendor Name"
wsOut.Cells(1, 2).Value = "Account"
wsOut.Cells(1, 3).Value = "Status"
StyleHeader wsOut.Rows(1)
' 3. Clear log and log the run start
GF_ClearLogSheet ThisWorkbook
GF_LogRowAction ThisWorkbook, "RunGenerateReport", "", "", 0, "", "", "START", _
"Output: " & outFile
' 4. Show progress bar and process rows
Dim lastRow As Long : lastRow = dataWS.UsedRange.Rows.Count
ProgressStart "Generating report...", lastRow - 1
Dim outRow As Long : outRow = 2
Dim i As Long
For i = 2 To lastRow
If ProgressCancelled() Then
GF_LogRowAction ThisWorkbook, "RunGenerateReport", "", "", 0, "", "", "ABORTED", _
"User cancelled at row " & i
GoTo CleanUp
End If
ProgressUpdate i - 1
Dim vendName As String : vendName = GF_StrictClean(NzStr(dataWS.Cells(i, 1).Value))
If Len(vendName) = 0 Then GoTo NextRow
Dim acctRaw As Variant : acctRaw = dataWS.Cells(i, 2).Value
Dim acctNorm As String : acctNorm = CanonicalizeAccountStr(CStr(acctRaw))
wsOut.Cells(outRow, 1).Value = vendName
wsOut.Cells(outRow, 2).Value = acctNorm
wsOut.Cells(outRow, 3).Value = "PROCESSED"
outRow = outRow + 1
NextRow:
Next i
' 5. Save the output file
ProgressMarquee "Saving file..."
ThisWorkbook.SaveCopyAs outFile
ProgressEndMarquee
GF_LogRowAction ThisWorkbook, "RunGenerateReport", "", "", 0, "", "", "DONE", _
"Rows written: " & outRow - 2
CleanUp:
ProgressFinish
End Sub
You need to email each vendor their bank account details. The email addresses come from a
spreadsheet and may be semicolon-separated lists, some invalid. The body template contains
a [[BANK_ACCOUNTS]] placeholder.
Modules needed: LIB1, LIB3, LIB5, LIB8
Dim i As Long
For i = 2 To lastRow
Dim rawEmail As String : rawEmail = NzStr(ws.Cells(i, colEmail).Value)
Dim toAddrs As String : toAddrs = NormalizeEmails(rawEmail)
If Len(toAddrs) = 0 Then
GF_LogRowAction ThisWorkbook, "SendVendorEmail", srcFile, "Data", i, _
NzStr(ws.Cells(i, colIndex).Value), "", _
"SKIPPED", "No valid email address in: [" & rawEmail & "]"
GoTo NextRow
End If
' Build accounts HTML from a comma-separated cell value:
Dim acctList As String : acctList = NzStr(ws.Cells(i, colAccounts).Value)
Dim acctHtml As String : acctHtml = BuildAccountsHtml(acctList)
' Compose full body from template with placeholder injection:
Dim tmplBody As String : tmplBody = NzStr(templateWS.Range("B2").Value)
Dim bodyHtml As String : bodyHtml = ComposeBodyHtml(tmplBody, acctHtml)
' Build attachment path (if configured):
Dim attachPath As String
attachPath = BuildAttachmentPath(ws.Cells(i, colAttachFolder).Value, _
ws.Cells(i, colAttachFile).Value)
' Send via Outlook:
Dim mail As Object
Set mail = CreateObject("Outlook.Application").CreateItem(0)
With mail
.To = toAddrs
.Subject = "Bank Account Verification — " & FormatDateLong(Date)
.HTMLBody = WrapHtml(bodyHtml)
If Len(attachPath) > 0 Then .Attachments.Add attachPath
.Send
End With
GF_LogRowAction ThisWorkbook, "SendVendorEmail", srcFile, "Data", i, _
NzStr(ws.Cells(i, colIndex).Value), "", _
"DONE", "Sent to: " & toAddrs
NextRow:
Next i
Your macro has a Run sheet where the user types an OU filter. They might type
611, AU_611, or AU_611_GDSAU_OU. The Oracle extract
has the full OU string. You need to filter rows where the OU matches, regardless of format.
Blank filter = include all rows.
Modules needed: LIB1, LIB9
Dim userOU As String
userOU = GF_StrictClean(NzStr(wsRun.Range("K6").Value))
' userOU could be "", "611", "AU_611", or "AU_611_GDSAU_OU"
Dim i As Long
For i = 2 To lastRow
Dim rowOrg As String
rowOrg = GF_StrictClean(NzStr(oracleWS.Cells(i, colOrg).Value))
If Not OrgMatches(rowOrg, userOU) Then GoTo NextRow
' Row's OU matches the filter (or filter is blank) — include it
' ... process the row ...
NextRow:
Next i
A file from a European supplier has prices in German format: 1.234,56 (dot
as thousands separator, comma as decimal). You need to convert them to US format
1234.56 before storing in Oracle.
Modules needed: LIB5, LIB7
' Pre-scan: count how many rows need fixing to warn the user first Dim issueCount As Long : issueCount = 0 Dim i As Long For i = 2 To lastRow If GF_PriceNeedsNormalization(dataWS.Cells(i, colPrice).Value) Then issueCount = issueCount + 1 End If Next i If issueCount > 0 Then Dim answer As VbMsgBoxResult answer = MsgBox(issueCount & " rows have non-standard price formatting." & vbCrLf & _ "Click YES to auto-convert, NO to skip those rows.", _ vbYesNo + vbExclamation, "Price Format Warning") If answer = vbNo Then MsgBox "Macro cancelled." Exit Sub End If End If ' Processing loop: For i = 2 To lastRow Dim rawPrice As Variant : rawPrice = dataWS.Cells(i, colPrice).Value Dim normPrice As String Dim wasChanged As Boolean, priceNote As String normPrice = GF_NormalizePriceToUS(rawPrice, wasChanged, priceNote) If Len(normPrice) = 0 Then GF_LogRowAction ThisWorkbook, "ImportPrices", srcFile, srcTab, i, _ NzStr(dataWS.Cells(i, colIndex).Value), "", _ "SKIPPED", "Unparseable price: [" & CStr(rawPrice) & "]" GoTo NextRow End If dataWS.Cells(i, colPrice).NumberFormat = "@" dataWS.Cells(i, colPrice).Value = normPrice If wasChanged Then GF_LogRowAction ThisWorkbook, "ImportPrices", srcFile, srcTab, i, _ NzStr(dataWS.Cells(i, colIndex).Value), "", _ "FIXED_AND_PROCESSED", "PRICE: [" & CStr(rawPrice) & "] -> [" & normPrice & "] " & priceNote End If NextRow: Next i
Worksheets("name") raises Runtime Error 9 if the sheet does not exist.
Always use GetSheetIfExists() or GetOrCreateSheet() from LIB2.
' WRONG — crashes if "Config" sheet is missing: Set wsConfig = ThisWorkbook.Worksheets("Config") ' RIGHT: Set wsConfig = GetSheetIfExists(ThisWorkbook, "Config") If wsConfig Is Nothing Then MsgBox "Config sheet missing": GoTo CleanUp
Excel error values (#N/A, #VALUE!) and Null values crash
CStr(). Always wrap cell reads with NzStr().
' WRONG — crashes if cell contains #N/A: Dim v As String : v = CStr(ws.Cells(i, c).Value) ' RIGHT: Dim v As String : v = NzStr(ws.Cells(i, c).Value)
CleanCompanyName and TokenSortKey do string replacement work on
every call. If you are comparing one query name against 5,000 Oracle rows, call them once
for the query name before the loop — not inside it.
' WRONG — computes the same query key 5000 times: For i = 2 To 5001 score = ScorePair(CleanCompanyName(queryName), TokenSortKey(CleanCompanyName(queryName)), _ oracleClean(i), oracleTok(i)) Next i ' RIGHT — compute once: Dim aClean As String : aClean = CleanCompanyName(queryName) Dim aTok As String : aTok = TokenSortKey(aClean) For i = 2 To 5001 score = ScorePair(aClean, aTok, oracleClean(i), oracleTok(i)) Next i
Worksheet Range().Value arrays are 1-based. Never access
index 0 on an array read from a worksheet range.
AppLock (the Application.ScreenUpdating / EnableEvents
/ DisplayAlerts / Calculation toggle) always lives in the
Orchestrator module. Step modules (Helpers, Extract, etc.) never call it directly.
See CLAUDE.md §9 for the canonical implementation.
Always call ProgressFinish from the CleanUp: label, not just at
the end of the normal execution path. If your macro errors out or the user aborts, the
form will stay open and block Excel.
' WRONG — ProgressFinish only on normal path: ProgressStart "Working...", 100 If someError Then Exit Sub ' form left open! ' RIGHT — ProgressFinish always reached: ProgressStart "Working...", 100 If someError Then GoTo CleanUp CleanUp: ProgressFinish