Repository navigation
MatrixParser
Type: Component (Service) Domain: Spreadsheet Processing / District Volume Import Status: Complete (Issue #905) Last updated: 2026-06-17
MatrixParser is the core parsing engine for two-row merged-header matrix spreadsheets. It resolves merged-cell group headers (row 1) and sub-column headers (row 2) into flat Record<string, unknown>[] output, producing one record per (district, group) combination.
Created as a generic, reusable component — not specific to district volumes. The parser accepts a MatrixConfig to describe the layout, making it usable for any two-row merged header format.
Stateless service. No constructor parameters.
Parses a worksheet into flat records.
| Parameter | Type | Required | Description |
|---|---|---|---|
worksheet |
ExcelJS.Worksheet |
yes | Worksheet from ExcelReader.read()
|
config |
MatrixConfig |
yes | Describes the matrix layout |
Returns: ParseResult<Record<string, unknown>> — { data, errors } where each record has keys [config.districtKey, config.groupOutputKey, ...subColumnKeys].
Iterates worksheet.model.merges looking for horizontal merges on config.groupHeaderRow. Each merge range from column A to column B creates an entry in groupLabels mapping every column in that range to the merge's top-left cell value.
For columns covered by config.groups but not resolved in Step 1, use the config's group.label as the group label.
Reads row config.columnHeaderRow to get sub-column header labels.
For each row from config.dataStartRow to end of sheet:
- Skips rows with empty district code
- For each
groupinconfig.groups, produces one record:-
[config.districtKey]= district code (columnconfig.districtCol) -
[config.groupOutputKey]= group label -
[subColumn.key]= raw cell value for each sub-column
-
| Field | Type | Description |
|---|---|---|
sheetName |
string |
Expected worksheet name |
headerRows |
number |
Total header rows (typically 2) |
groupHeaderRow |
number |
Row number of the merged group header (1-based) |
columnHeaderRow |
number |
Row number of the sub-column headers (1-based) |
dataStartRow |
number |
First data row (1-based) |
groups |
ColumnGroup[] |
Column groups defining zones/sections |
districtCol |
number |
Column index of the district code (1-based) |
districtKey |
string |
Output key for the district value |
groupOutputKey |
string |
Output key for the group label |
| Field | Type | Description |
|---|---|---|
label |
string |
Display name of the group (zone/section) |
colStart |
number |
Start column index (1-based, inclusive) |
colEnd |
number |
End column index (1-based, inclusive) |
subColumns |
SubColumnDef[] |
Ordered sub-column definitions |
| Field | Type | Description |
|---|---|---|
header |
string |
Expected header text (whitespace-normalized) |
key |
string |
Output key for this value column |
The same parser can handle any two-row merged-header matrix format. District volume configs (interiorMatrixConfig, coastMatrixConfig) are just data.
Uses exceljs's native merge-range model (worksheet.model.merges) to propagate group header values across merged cell ranges.
Cell text is cleaned via replace(/\s+/g, ' ').trim() to match the normalization in validators and configs.
| Dependency | Purpose |
|---|---|
exceljs |
Worksheet access and merge-range model |
types.ts (local) |
MatrixConfig, ParseResult, ColumnGroup, SubColumnDef
|
| Constraint | Detail |
|---|---|
| Two-row headers | Row 1 = merged group labels, Row 2 = sub-column headers |
| Enforces none | Parser does not validate — returns all values as-is with null for missing cells |
| ASCII column letters |
columnLetterToIndex only handles A-Z (single and multi-letter) |
| SheetJS not used | Relies on exceljs merge model, not SheetJS/XLSX |
| Artifact | Path |
|---|---|
| Service | frontend/src/services/spreadsheet/matrixParser.ts |
| Types | frontend/src/services/spreadsheet/types.ts |
| Tests | frontend/src/services/spreadsheet/matrixParser.unit.test.ts |
6 tests covering: single district (one per zone), multiple districts, empty district skip, null cells, headers-only (empty data), and coast format.
| Consumer | Location |
|---|---|
| DistrictVolumeProcessor | src/services/districtvolumes/processors/ |
| (Any future matrix-format parser) | — |
import { ExcelReader } from '@/services/spreadsheet/excelReader';
import { MatrixParser } from '@/services/spreadsheet/matrixParser';
import { interiorMatrixConfig } from '@/services/districtvolumes/config/interiorMatrixConfig';
const reader = new ExcelReader();
const worksheet = await reader.read(file, 'Interior');
const parser = new MatrixParser();
const result = parser.parse(worksheet, interiorMatrixConfig);
// result.data = [
// { district: 'DCC', zone: 'Dry belt', avoidableSawlog: 2.04, avoidableGradeY4: 7.05, unavoidable: 0.08, total: 9.17 },
// { district: 'DCC', zone: 'Transition zone', avoidableSawlog: 7.96, ... },
// { district: 'DCC', zone: 'Wet belt', avoidableSawlog: 13.5, ... },
// ]| Reference | Link |
|---|---|
| GitHub Issue | #905 |
| Related page | ExcelReader |
| Related page | SpreadsheetValidator |
| Related page | DistrictVolumeProcessor |
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository