Home Manual Reference Source

src/parseBundle.js

import assert from 'node:assert';
import * as tape from '@async-abstraction/tape';
import {asyncIterableToArray} from '@async-iterable-iterator/async-iterable-to-array';
import {ll1, ast} from '@formal-language/grammar';

import tokens from './tokens.js';
import grammar from './grammar.js';
import simplify from './transform/simplify.js';
import {iter, next, map as asyncMap} from './transform/lib.js';

const parseTape = (inputTape) => {
	const parser = ll1.from(grammar);
	const inputTokens = tokens(inputTape);
	const inputTokensTape = tape.fromAsyncIterable(inputTokens);
	const tree = parser.parse(inputTokensTape);

	const ctx = {};

	return ast.transform(tree, simplify, ctx);
};

const parseString = (string) => {
	const inputCharacterTape = tape.fromString(string);
	return parseTape(inputCharacterTape);
};

const parseBlockBegin = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === '#R');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			type: line.slice(2).trim(),
		},
	};
};

const parseBlockTitle = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'R-title');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			title: line.trim(),
		},
	};
};

const parseBlockType = (type) => {
	switch (type) {
		case 'd': {
			return {TimeIndication: 'Days'};
		}

		case 'h': {
			return {TimeIndication: 'Hours'};
		}

		case 'm': {
			return {TimeIndication: 'Minutes'};
		}

		case 's': {
			return {TimeIndication: 'Seconds'};
		}

		default: {
			assert(type === 'a');
			return {};
		}
	}
};

const parseIntensity = (symbol) => {
	switch (symbol) {
		case '--':
		case 'LL':
		case '1': {
			return 'GreatlyReduced';
		}

		case '-':
		case 'L':
		case '2': {
			return 'Reduced';
		}

		case '=':
		case 'N':
		case '3':
		case '': {
			return 'Normal';
		}

		case '+':
		case 'H':
		case '4': {
			return 'Increased';
		}

		case '++':
		case 'HH':
		case '5': {
			return 'GreatlyIncreased';
		}

		default: {
			return 'Unknown';
		}
	}
};

const parseBlockComments = (comments) => {
	if (comments.length > 0 && comments[0].slice(0, 1) === '\\') {
		return {
			referenceValue: comments[0].slice(1),
			comments: comments.slice(1),
		};
	}

	return {
		comments,
	};
};

const parseBlockBody = (type, tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'R-body');
	if (type === 'c') {
		return {
			...tree,
			parsed: {
				comments: tree.lines.map(({contents}) => contents),
			},
		};
	}

	if (type === 'b') {
		return {
			...tree,
			parsed: {
				text: tree.lines.map(({contents}) => contents),
			},
		};
	}

	assert(tree.lines.length >= 3);
	const [line1, line2, line3, ...rest] = tree.lines.map(
		({contents}) => contents,
	);
	const intensitySymbol = line3.trim();
	return {
		...tree,
		parsed: {
			...parseBlockType(type),
			relation: line1.slice(0, 1),
			value: line1.slice(1).trim(),
			unit: line2.trim(),
			intensity: parseIntensity(intensitySymbol),
			intensitySymbol,
			...parseBlockComments(rest),
		},
	};
};

const parseBlock = async (block) => {
	assert(block.type === 'node');
	assert(block.nonterminal === 'R');
	const it = iter(block.children);
	const begin = parseBlockBegin(await next(it));
	const title = parseBlockTitle(await next(it));
	const contents = parseBlockBody(begin.parsed.type, await next(it));
	const end = await next(it);
	return {
		begin,
		title,
		contents,
		end,
	};
};

const parseSex = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'A-sex');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	const sex = line === 'X' ? 'female' : line === 'Y' ? 'male' : 'other';
	return {
		...tree,
		parsed: {
			sex,
		},
	};
};

const parseReference = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'A-reference');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			reference: line.trim(),
		},
	};
};

const parseProtocolCode = (code) => {
	switch (code) {
		case 'P':
		case 'S':
		case 'L': {
			return 'partial';
		}

		case 'C': {
			return 'complete';
		}

		default: {
			return '';
		}
	}
};

const parseCode = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'A-code');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	const code = line.trim();
	const status = parseProtocolCode(code);
	return status === ''
		? {
				...tree,
				parsed: {
					code,
				},
		  }
		: {
				...tree,
				parsed: {
					code,
					status,
				},
		  };
};

const parseReportIdentifier = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === '#A');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	const identifier = line.slice(2);
	return identifier.length === 11
		? {
				...tree,
				parsed: {
					nn: identifier,
				},
		  }
		: identifier.length === 13
		? {
				...tree,
				parsed: {
					dossier: identifier,
				},
		  }
		: {
				...tree,
				parsed: {
					identifier,
				},
		  };
};

const parseReportFooter = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === '#A/');
	assert(tree.lines.length === 1);
	return tree;
};

const parseReport = async (report) => {
	assert(report.type === 'node');
	assert(report.nonterminal === 'A');
	const it = iter(report.children);
	const identifier = parseReportIdentifier(await next(it));
	const name = parseName(await next(it));
	const birthdate = parseDate(await next(it));
	const sex = parseSex(await next(it));
	const requestDate = parseDate(await next(it));
	const reference = parseReference(await next(it));
	const code = parseCode(await next(it));
	const extra = parseExtra(await next(it));
	const header = {
		identifier,
		name,
		birthdate,
		sex,
		requestDate,
		reference,
		code,
		extra,
	};

	const blocks = await next(it);

	const parsedBlocks = await asyncIterableToArray(
		asyncMap(parseBlock, blocks.children),
	);

	const footer = parseReportFooter(await next(it));
	return {
		header,
		blocks: parsedBlocks,
		footer,
	};
};

const parseNIHDI = (tree) => {
	assert(tree.type === 'leaf');
	assert(
		tree.terminal === 'doctor-nihdi' || tree.terminal === 'requestor-nihdi',
	);
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			nihdi: line.replaceAll('/', ''),
		},
	};
};

const parseName = (tree) => {
	assert(tree.type === 'leaf');
	assert(
		tree.terminal === 'doctor-name' ||
			tree.terminal === 'requestor-name' ||
			tree.terminal === 'A-name',
	);
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			firstname: line.slice(24).trim(),
			lastname: line.slice(0, 24).trim(),
		},
	};
};

const parseDoctorAddress = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'doctor-address');
	assert(tree.lines.length === 2);
	const [line1, line2] = tree.lines.map(({contents}) => contents);
	return {
		...tree,
		parsed: {
			streetName: line1.slice(0, 35).trim(),
			streetNumber: line1.slice(35).trim(),
			postalCode: line2.slice(0, 10).trim(),
			townName: line2.slice(10).trim(),
		},
	};
};

const parseLabAddress = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'lab-address');
	assert(tree.lines.length === 2);
	const [address1, address2] = tree.lines.map(({contents}) => contents);
	return {
		...tree,
		parsed: {
			address1,
			address2,
		},
	};
};

const parsePhone = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'doctor-phone');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			phone: line.trim(),
		},
	};
};

const parseExtra = (tree) => tree;
const parseLabIdentifier = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'lab-identifier');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			identifier: line.trim(),
		},
	};
};

const parseLabName = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'lab-name');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			name: line.trim(),
		},
	};
};

const parseLab = async (tree) => {
	assert(tree.type === 'node');
	assert(tree.nonterminal === 'lab');
	const it = iter(tree.children);
	const identifier = parseLabIdentifier(await next(it));
	const name = parseLabName(await next(it));
	const address = parseLabAddress(await next(it));
	const extra = parseExtra(await next(it));
	return {
		type: 'leaf',
		terminal: 'lab',
		identifier,
		name,
		address,
		extra,
	};
};

const parseDoctor = async (tree) => {
	assert(tree.type === 'node');
	assert(tree.nonterminal === 'doctor');
	const it = iter(tree.children);
	const nihdi = parseNIHDI(await next(it));
	const name = parseName(await next(it));
	const address = parseDoctorAddress(await next(it));
	const phone = parsePhone(await next(it));
	const extra = parseExtra(await next(it));
	return {
		type: 'leaf',
		terminal: 'doctor',
		nihdi,
		name,
		address,
		phone,
		extra,
	};
};

const parseDatetime = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'date');
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return line.length >= 9
		? {
				...tree,
				parsed: {
					date: line.slice(0, 8),
					time: line.slice(8),
				},
		  }
		: parseDate(tree);
};

const parseDate = (tree) => {
	assert(tree.type === 'leaf');
	assert(
		tree.terminal === 'date' ||
			tree.terminal === 'A-birthdate' ||
			tree.terminal === 'A-date',
	);
	assert(tree.lines.length === 1);
	const line = tree.lines[0].contents;
	return {
		...tree,
		parsed: {
			date: line,
		},
	};
};

const parseRequestor = async (tree) => {
	assert(tree.type === 'node');
	assert(tree.nonterminal === 'requestor');
	const it = iter(tree.children);
	const nihdi = parseNIHDI(await next(it));
	const name = parseName(await next(it));
	return {
		type: 'leaf',
		terminal: 'requestor',
		nihdi,
		name,
	};
};

const parseDocumentFooter = (tree) => {
	assert(tree.type === 'leaf');
	assert(tree.terminal === 'footer');
	assert(tree.lines.length === 1);
	return tree;
};

const parseDocument = async (document) => {
	assert(document.type === 'node');
	assert(document.nonterminal === 'document');
	const kind = document.production;
	const it = iter(document.children);
	const requesteeKind = kind === 'lab' ? 'lab' : 'doctor';
	const parseRequestee = kind === 'lab' ? parseLab : parseDoctor;
	const requestee = await parseRequestee(await next(it));
	const date = parseDatetime(await next(it));
	const requestor = await parseRequestor(await next(it));
	const header = {
		kind,
		[requesteeKind]: requestee,
		date,
		requestor,
	};

	const reports = await next(it);

	const parsedReports = await asyncIterableToArray(
		asyncMap(parseReport, reports.children),
	);

	const footer = parseDocumentFooter(await next(it));

	return {
		header,
		reports: parsedReports,
		footer,
	};
};

const parseTree = (tree) => {
	assert(tree.type === 'node');
	assert(tree.nonterminal === 'documents');
	return asyncMap(parseDocument, tree.children);
};

const parseBundle = async (string) => {
	const root = await parseString(string);
	const tree = await next(iter(root.children));
	return parseTree(tree);
};

export default parseBundle;