bc3
v1.1.0
Published
Zero-dependency TypeScript parser for FIEBDC-3/BC3 construction budget files (Presto, ARQUIMEDES, TCQ). Builds a type-safe hierarchical model for bills of quantities, cost databases, and construction project budgets.
Maintainers
Readme
BC3 — FIEBDC-3 Parser for TypeScript & Node.js
BC3 is a zero-dependency TypeScript parser for FIEBDC-3 / BC3 construction budget files — the standard format exported by Presto, ARQUIMEDES, TCQ, and Spanish-speaking construction software for bills of quantities (BOQ), cost databases, and project budgets.
npm install bc3Parse .bc3 files into a type-safe hierarchical model — navigate
chapters, subchapters, decomposition trees, measurements, entities,
specifications, IT/BIM codes, geographic cost overrides, and cost
coefficients with lossless fidelity to the original file.
Why BC3
FIEBDC-3 (also called BC3 or Formato BC3) is the standard exchange
format for construction budgets in Spain and Latin America. Software like
Presto, ARQUIMEDES, and TCQ exports .bc3 files — but no
open-source TypeScript/JavaScript library could parse them into a usable
data structure. BC3 fills that gap.
- Parse
.bc3files from any FIEBDC-3 edition (2002–2020) out of the box - Walk chapters, subchapters, and decompositions as a typed tree
- Extract prices, measurements, BIM codes, cost overrides, entities, and coefficients from every record type in the corpus
- Zero runtime dependencies — works in Node.js, Deno, and the browser
- Lossless fidelity to the original file; no data discarded
Status
Stable release — 15 of 16 record types parsed, 100% of observed corpus types supported, 147 regression tests. 7/7 real-world BC3 files parse with 0 errors.
Current version: v1.0.0
Installation
npm install bc3Quick Start
import { BC3 } from 'bc3';
const result = BC3.parse(bc3Text, { mode: 'lenient' });
if (result.document) {
console.log(`Root concepts: ${result.document.roots.length}`);
console.log(`Total concepts: ${result.document.conceptsByCode.size}`);
console.log(`Diagnostics: ${result.diagnostics.length}`);
// Walk the hierarchy
result.document.walkTree((node, depth) => {
console.log(
`${' '.repeat(depth)}${node.concept.codeNorm} — ${node.concept.summary}`,
);
});
}Features
Parsing
- 15 record-type parsers —
~Vthrough~G, plusUnknownRecordParser - Real-world corpus support — tested against 7 BC3 files from Presto, ARQUIMEDES, and TCQ spanning FIEBDC-3/2002–2020
- Multiline ~D records — ARQUIMEDES continuation lines parsed correctly
- Dotted child codes —
WORKER.1a,I.LT04.01handled correctly in decompositions - Geographic cost overrides —
~Orecords with location/price pairs - Dual parsing modes —
'lenient'(collect diagnostics, continue) and'strict'(fail on first error) - Encoding-agnostic — accepts UTF-16 strings; callers decode Latin-1 before calling
BC3.parse()
Domain model
- Hierarchy tree — chapters, subchapters, and items as
ConceptNodeinstances with parent-child traversal - Multiple occurrences — concepts appearing in multiple branches tracked correctly
- Decompositions — factor, performance (rendimiento), and percentage codes
- Measurements — positions, totals, BIM IDs, dimensions
- Metadata — document version, generator, dates, and properties from
~V - Cost coefficients — currency, rounding, overhead rates from
~K - Entities — companies, persons, and contacts from
~E - Specifications — pliegos sections from
~L - IT / BIM codes — BIM parameters, LCA environmental data from
~X - Thesaurus — keyword classification from
~A - Geographic overrides — regional cost adjustments from
~O - Diagnostics — warnings and errors with codes, messages, and record positions
Architecture
- Zero runtime dependencies — pure TypeScript, no npm dependencies
- ESM-native — dual CJS/ESM output via tsup
- TypeScript strict mode —
noUncheckedIndexedAccess,noImplicitOverride - Builder pattern — clean separation between parsing and domain assembly
- Strategy pattern — one parser class per record type, independently testable
API Reference
BC3.parse(input, options?)
Parses a BC3 text input and returns a structured result.
const result: ParseResult = BC3.parse(input: string, options?: {
mode?: 'strict' | 'lenient'; // default: 'lenient'
});
interface ParseResult {
document?: BC3Document;
diagnostics: Diagnostic[];
}Encoding: BC3 corpus files use ISO-8859-1 (Latin-1). Callers must decode before parsing:
import fs from 'node:fs'; const input = fs.readFileSync('file.bc3', 'latin1'); const result = BC3.parse(input, { mode: 'lenient' });
BC3Document
| Property | Type | Description |
| -------------------------- | ------------------------------- | -------------------------------------- |
| roots | ConceptNode[] | Root nodes of the hierarchy |
| conceptsByCode | Map<string, ConceptNode> | All concepts, keyed by normalized code |
| metadata | DocumentMetadata \| undefined | Version, generator, dates from ~V |
| coefficients | Coefficients \| undefined | Currency, rounding, overhead from ~K |
| costOverrides | Map<string, CostOverride> | Geographic price adjustments from ~O |
| entities | Map<string, Entity> | Companies and contacts from ~E |
| specificationsDictionary | Specification \| undefined | Pliegos sections from ~L |
| itCodesDictionary | ITCodes \| undefined | IT/BIM/LCA codes from ~X |
| diagnostics | Diagnostic[] | Warnings and errors |
| Method | Description |
| ------------------------------------- | --------------------------------------------------------------------- |
| getConcept(code) | Lookup a ConceptNode by normalized code |
| walkTree(visitor) | DFS traversal with (node, depth, path) callback |
| getHierarchySummary() | { totalNodes, rootNodes, maxDepth, nodesByDepth } |
| getAllPathsToConcept(code) | All paths from roots to a concept |
| getParentNodes(code) | All parent nodes of a concept |
| getChildNodes(code) | Direct children |
| getDecompositionInfo(parent, child) | { performance?, factor? } |
| countConceptOccurrences(code) | Occurrence count in the tree |
| getResourceHierarchy() | Concepts grouped by resource type (labor, machinery, materials, etc.) |
ConceptNode
| Property | Type | Description |
| ---------------- | ---------------------------- | ---------------------------------------------- |
| concept | Concept | Code, unit, summary, prices, dates, type, text |
| children | ConceptNode[] | Direct children in hierarchy |
| decompositions | Decomposition[] | Parent-child economic relationships |
| measurements | Measurement[] | Associated measurements |
| specification | Specification \| undefined | Pliego sections |
| itCodes | ITCodes \| undefined | IT/BIM codes |
| thesaurus | Thesaurus \| undefined | Keywords |
Examples
Navigate the hierarchy
const result = BC3.parse(bc3Text);
const doc = result.document;
if (doc) {
const summary = doc.getHierarchySummary();
console.log(
`Roots: ${summary.rootNodes}, Total: ${summary.totalNodes}, Depth: ${summary.maxDepth}`,
);
doc.walkTree((node, depth) => {
console.log(
`${' '.repeat(depth)}${node.concept.codeNorm} — ${node.concept.summary}`,
);
});
}Find concept occurrences
const paths = doc.getAllPathsToConcept('001010');
paths.forEach((path, i) => {
console.log(
`Path ${i + 1}: ${path.map((n) => n.concept.codeNorm).join(' → ')}`,
);
});
const decompInfo = doc.getDecompositionInfo('300100', '001010');
if (decompInfo) {
const child = doc.getConcept('001010')!;
const price = child.concept.prices.at(-1)!;
console.log(`Amount: ${price * (decompInfo.performance ?? 0)}`);
}Parse a Latin-1 encoded file (Node.js)
import fs from 'node:fs';
import { BC3 } from 'bc3';
const input = fs.readFileSync('file.bc3', 'latin1');
const result = BC3.parse(input, { mode: 'lenient' });
// Check diagnostics
for (const d of result.diagnostics) {
console.log(`[${d.level}] ${d.code}: ${d.message}`);
}Parsing Modes
| Mode | Unknown records | Missing fields | Invalid values | Behavior |
| ------------------- | --------------- | --------------- | --------------- | ------------------------- |
| lenient (default) | Warn + skip | Warn + continue | Warn + continue | Always returns a document |
| strict | Throw | Throw | Throw | Fails on first error |
Architecture & Documentation
| Topic | Document |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| Architecture overview | docs/architecture/overview.md |
| Module boundaries | docs/architecture/module-boundaries.md |
| Design patterns | docs/architecture/design-patterns.md |
| BC3 grammar | docs/parser/grammar.md |
| Record parsers | docs/parser/record-parsers.md |
| Parsing modes | docs/parser/parsing-modes.md |
| Parser coverage | docs/parser/parser-coverage-matrix.md |
| Hierarchy reconstruction | docs/parser/hierarchy-reconstruction.md |
| Domain model | docs/domain/model.md |
| Public API | docs/public-api.md |
| Usage examples | docs/examples.md |
| Development setup | docs/development/setup.md |
| Roadmap | docs/development/work-to-issue-mapping.md |
| ADRs | docs/decisions/index.md |
Project Workflow
- Development on
develop; production onmain - Branch naming:
feat/,fix/,test/,docs/,chore/prefixed with issue number - Changesets for versioning; automated npm publish on merge to
main npm run cigates all PRs: build + format check
Related Terms
| English | Español | | ---------------------------------- | ----------------------------------------- | | bill of quantities (BOQ) | presupuesto, mediciones | | cost database | base de precios, banco de precios, cuadro | | construction budget | presupuesto de obra, proyecto | | cost estimation | valoración, estimación de costes | | decomposition, work breakdown | descomposición, descompuestos | | FIEBDC-3, BC3, Formato BC3 | FIEBDC-3, BC3, Formato BC3 | | Presto, ARQUIMEDES, TCQ | Presto, ARQUIMEDES, TCQ | | TypeScript parser, Node.js library | librería TypeScript, parser Node.js | | concept tree, hierarchy | árbol de conceptos, jerarquía | | overhead, indirect costs | costes indirectos, gastos generales | | geographic cost override | cuadro de precios geográfico | | measurements, quantities | cantidades, líneas de medición | | price list, unit prices | lista de precios, precios unitarios |
License
MIT © Igor HC
