@fin.cx/opendata 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +6 -2
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/financialstatements/amounts.d.ts +58 -0
- package/dist_ts/financialstatements/amounts.js +130 -0
- package/dist_ts/financialstatements/classes.financialstatementerror.d.ts +24 -0
- package/dist_ts/financialstatements/classes.financialstatementerror.js +10 -0
- package/dist_ts/financialstatements/constants.d.ts +16 -0
- package/dist_ts/financialstatements/constants.js +20 -0
- package/dist_ts/financialstatements/dates.d.ts +16 -0
- package/dist_ts/financialstatements/dates.js +72 -0
- package/dist_ts/financialstatements/html.d.ts +21 -0
- package/dist_ts/financialstatements/html.js +92 -0
- package/dist_ts/financialstatements/index.d.ts +6 -0
- package/dist_ts/financialstatements/index.js +11 -0
- package/dist_ts/financialstatements/interfaces.d.ts +258 -0
- package/dist_ts/financialstatements/interfaces.js +2 -0
- package/dist_ts/financialstatements/layout.d.ts +20 -0
- package/dist_ts/financialstatements/layout.html.d.ts +8 -0
- package/dist_ts/financialstatements/layout.html.js +125 -0
- package/dist_ts/financialstatements/layout.js +2 -0
- package/dist_ts/financialstatements/layout.pdf.d.ts +6 -0
- package/dist_ts/financialstatements/layout.pdf.js +229 -0
- package/dist_ts/financialstatements/mediatypes.d.ts +3 -0
- package/dist_ts/financialstatements/mediatypes.js +11 -0
- package/dist_ts/financialstatements/outline.d.ts +22 -0
- package/dist_ts/financialstatements/outline.js +26 -0
- package/dist_ts/financialstatements/parse.esef.d.ts +13 -0
- package/dist_ts/financialstatements/parse.esef.js +349 -0
- package/dist_ts/financialstatements/parse.hgb.d.ts +9 -0
- package/dist_ts/financialstatements/parse.hgb.js +1014 -0
- package/dist_ts/financialstatements/parse.statement.d.ts +8 -0
- package/dist_ts/financialstatements/parse.statement.js +25 -0
- package/dist_ts/financialstatements/plugins.d.ts +10 -0
- package/dist_ts/financialstatements/plugins.js +15 -0
- package/dist_ts/financialstatements/read.d.ts +10 -0
- package/dist_ts/financialstatements/read.js +113 -0
- package/package.json +14 -4
- package/readme.md +82 -4
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/financialstatements/amounts.ts +181 -0
- package/ts/financialstatements/classes.financialstatementerror.ts +35 -0
- package/ts/financialstatements/constants.ts +24 -0
- package/ts/financialstatements/dates.ts +89 -0
- package/ts/financialstatements/html.ts +107 -0
- package/ts/financialstatements/index.ts +17 -0
- package/ts/financialstatements/interfaces.ts +268 -0
- package/ts/financialstatements/layout.html.ts +130 -0
- package/ts/financialstatements/layout.pdf.ts +268 -0
- package/ts/financialstatements/layout.ts +16 -0
- package/ts/financialstatements/mediatypes.ts +14 -0
- package/ts/financialstatements/outline.ts +44 -0
- package/ts/financialstatements/parse.esef.ts +394 -0
- package/ts/financialstatements/parse.hgb.ts +1235 -0
- package/ts/financialstatements/parse.statement.ts +26 -0
- package/ts/financialstatements/plugins.ts +29 -0
- package/ts/financialstatements/read.ts +135 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { FinancialStatementError } from './classes.financialstatementerror.js';
|
|
3
|
+
import { maxPdfPages } from './constants.js';
|
|
4
|
+
import { normalizeSpaces, readsAsAmount } from './amounts.js';
|
|
5
|
+
import { isMemoLabel, splitOutlineCode } from './outline.js';
|
|
6
|
+
import type { ILayoutCell, TLayoutBlock } from './layout.js';
|
|
7
|
+
|
|
8
|
+
/** a run of text on a page, in page coordinates with y growing downwards */
|
|
9
|
+
interface IPiece {
|
|
10
|
+
text: string;
|
|
11
|
+
x0: number;
|
|
12
|
+
x1: number;
|
|
13
|
+
y: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** pieces of one line close enough to read as one: a label, an amount, a column heading */
|
|
18
|
+
interface ISegment {
|
|
19
|
+
text: string;
|
|
20
|
+
x0: number;
|
|
21
|
+
x1: number;
|
|
22
|
+
height: number;
|
|
23
|
+
amount: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** the amounts of a page that share a right edge: one column */
|
|
27
|
+
interface ICluster {
|
|
28
|
+
x0: number;
|
|
29
|
+
x1: number;
|
|
30
|
+
members: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** a row of a page as it is built, with where its label starts */
|
|
34
|
+
interface IPageRow {
|
|
35
|
+
cells: ILayoutCell[];
|
|
36
|
+
labelX0: number | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** the largest gap between pieces of one segment, and the gap that reads as a space, in font heights */
|
|
40
|
+
const segmentGap = 0.6;
|
|
41
|
+
const spaceGap = 0.15;
|
|
42
|
+
/** amounts whose right edges lie further apart than this are in different columns, in points */
|
|
43
|
+
const clusterGap = 12;
|
|
44
|
+
|
|
45
|
+
/** a label that a wrapped line continues: it ends on a hyphen, a comma or a word that needs another */
|
|
46
|
+
const continuedLabelPattern =
|
|
47
|
+
/(?:[-,]|\b(?:und|oder|sowie|aus|für|an|gegenüber|mit|von|zu|bei|auf|in|im|am|der|die|das|des|dem|den|ein|eine|einem|einen|einer|eines|ähnliche|ähnlichen|sonstige|sonstigen|denen))$/i;
|
|
48
|
+
|
|
49
|
+
const isContinuation = (previousLabelArg: string, labelArg: string): boolean =>
|
|
50
|
+
!isMemoLabel(labelArg) &&
|
|
51
|
+
splitOutlineCode(labelArg).code === null &&
|
|
52
|
+
(continuedLabelPattern.test(previousLabelArg) || /^[a-zäöüß]/.test(labelArg));
|
|
53
|
+
|
|
54
|
+
const linesOf = (piecesArg: IPiece[]): ISegment[][] => {
|
|
55
|
+
const sorted = [...piecesArg].sort((leftArg, rightArg) => leftArg.y - rightArg.y || leftArg.x0 - rightArg.x0);
|
|
56
|
+
const lines: IPiece[][] = [];
|
|
57
|
+
let lineY = Number.NEGATIVE_INFINITY;
|
|
58
|
+
for (const piece of sorted) {
|
|
59
|
+
const current = lines[lines.length - 1];
|
|
60
|
+
if (current && Math.abs(piece.y - lineY) <= Math.max(2, 0.3 * piece.height)) {
|
|
61
|
+
current.push(piece);
|
|
62
|
+
} else {
|
|
63
|
+
lines.push([piece]);
|
|
64
|
+
lineY = piece.y;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return lines.map((lineArg) => {
|
|
68
|
+
const segments: ISegment[] = [];
|
|
69
|
+
for (const piece of lineArg.sort((leftArg, rightArg) => leftArg.x0 - rightArg.x0)) {
|
|
70
|
+
const segment = segments[segments.length - 1];
|
|
71
|
+
const gap = segment ? piece.x0 - segment.x1 : Number.POSITIVE_INFINITY;
|
|
72
|
+
const height = Math.max(piece.height, segment?.height ?? 0);
|
|
73
|
+
if (segment && gap <= segmentGap * height) {
|
|
74
|
+
segment.text += gap > spaceGap * height ? ` ${piece.text}` : piece.text;
|
|
75
|
+
segment.x1 = Math.max(segment.x1, piece.x1);
|
|
76
|
+
segment.height = height;
|
|
77
|
+
} else {
|
|
78
|
+
segments.push({ text: piece.text, x0: piece.x0, x1: piece.x1, height: piece.height, amount: false });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const segment of segments) {
|
|
82
|
+
segment.text = normalizeSpaces(segment.text);
|
|
83
|
+
segment.amount = readsAsAmount(segment.text);
|
|
84
|
+
}
|
|
85
|
+
return segments.filter((segmentArg) => segmentArg.text !== '');
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The columns of a page: amounts sharing a right edge, at least two of them and one a number, so
|
|
91
|
+
* that dashes set as bullets make no column.
|
|
92
|
+
*/
|
|
93
|
+
const clustersOf = (linesArg: ISegment[][]): ICluster[] => {
|
|
94
|
+
const amounts = linesArg.flat().filter((segmentArg) => segmentArg.amount).sort((leftArg, rightArg) => leftArg.x1 - rightArg.x1);
|
|
95
|
+
const clusters: Array<ICluster & { lastX1: number; numbers: number }> = [];
|
|
96
|
+
for (const amount of amounts) {
|
|
97
|
+
const cluster = clusters[clusters.length - 1];
|
|
98
|
+
const number = /\d/.test(amount.text) ? 1 : 0;
|
|
99
|
+
if (cluster && amount.x1 - cluster.lastX1 <= clusterGap) {
|
|
100
|
+
cluster.x0 = Math.min(cluster.x0, amount.x0);
|
|
101
|
+
cluster.x1 = Math.max(cluster.x1, amount.x1);
|
|
102
|
+
cluster.lastX1 = amount.x1;
|
|
103
|
+
cluster.members++;
|
|
104
|
+
cluster.numbers += number;
|
|
105
|
+
} else {
|
|
106
|
+
clusters.push({ x0: amount.x0, x1: amount.x1, lastX1: amount.x1, members: 1, numbers: number });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return clusters
|
|
110
|
+
.filter((clusterArg) => clusterArg.members >= 2 && clusterArg.numbers > 0)
|
|
111
|
+
.map(({ x0, x1, members }) => ({ x0, x1, members }));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const overlaps = (segmentArg: ISegment, clusterArg: ICluster): boolean =>
|
|
115
|
+
segmentArg.x1 > clusterArg.x0 + 1 && segmentArg.x0 < clusterArg.x1 - 1;
|
|
116
|
+
|
|
117
|
+
/** one page as a table: column 0 the labels, columns 1 to n its amount columns */
|
|
118
|
+
const pageTable = (linesArg: ISegment[][]): ILayoutCell[][] => {
|
|
119
|
+
const clusters = clustersOf(linesArg);
|
|
120
|
+
const labelEnd = clusters.length > 0 ? clusters[0].x0 - 1 : Number.POSITIVE_INFINITY;
|
|
121
|
+
const clusterOfAmount = (segmentArg: ISegment): number =>
|
|
122
|
+
clusters.findIndex((clusterArg) => segmentArg.x1 >= clusterArg.x0 && segmentArg.x1 <= clusterArg.x1 + 1);
|
|
123
|
+
const rows: IPageRow[] = [];
|
|
124
|
+
for (const line of linesArg) {
|
|
125
|
+
const columns = new Map<number, { text: string[]; first: number; last: number }>();
|
|
126
|
+
const place = (columnArg: number, lastArg: number, textArg: string) => {
|
|
127
|
+
const cell = columns.get(columnArg) ?? { text: [], first: columnArg, last: lastArg };
|
|
128
|
+
cell.text.push(textArg);
|
|
129
|
+
cell.last = Math.max(cell.last, lastArg);
|
|
130
|
+
columns.set(columnArg, cell);
|
|
131
|
+
};
|
|
132
|
+
const amountColumns = line.map((segmentArg) => (segmentArg.amount ? clusterOfAmount(segmentArg) : -1));
|
|
133
|
+
const hasAmounts = amountColumns.some((columnArg) => columnArg >= 0);
|
|
134
|
+
let labelX0: number | null = null;
|
|
135
|
+
line.forEach((segment, index) => {
|
|
136
|
+
if (amountColumns[index] >= 0) {
|
|
137
|
+
place(amountColumns[index] + 1, amountColumns[index] + 1, segment.text);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const covered = clusters.flatMap((clusterArg, clusterIndex) => (overlaps(segment, clusterArg) ? [clusterIndex + 1] : []));
|
|
141
|
+
if (segment.x0 < labelEnd) {
|
|
142
|
+
labelX0 ??= segment.x0;
|
|
143
|
+
place(0, hasAmounts || covered.length === 0 ? 0 : Math.max(...covered), segment.text);
|
|
144
|
+
} else if (covered.length > 0) {
|
|
145
|
+
place(covered[0], covered[covered.length - 1], segment.text);
|
|
146
|
+
} else {
|
|
147
|
+
const center = (segment.x0 + segment.x1) / 2;
|
|
148
|
+
let nearest = 0;
|
|
149
|
+
clusters.forEach((clusterArg, clusterIndex) => {
|
|
150
|
+
const distance = Math.abs((clusterArg.x0 + clusterArg.x1) / 2 - center);
|
|
151
|
+
const best = Math.abs((clusters[nearest].x0 + clusters[nearest].x1) / 2 - center);
|
|
152
|
+
if (distance < best) {
|
|
153
|
+
nearest = clusterIndex;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
place(nearest + 1, nearest + 1, segment.text);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const cells = [...columns.values()]
|
|
160
|
+
.sort((leftArg, rightArg) => leftArg.first - rightArg.first)
|
|
161
|
+
.map((cellArg) => ({ text: normalizeSpaces(cellArg.text.join(' ')), first: cellArg.first, last: cellArg.last }));
|
|
162
|
+
if (cells.length > 0) {
|
|
163
|
+
rows.push({ cells, labelX0 });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return joinWrappedLabels(rows);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Joins labels a page wraps over two lines: a coded line without amounts whose next line carries on
|
|
171
|
+
* its label and holds the amounts, and a line whose label carries on on the next line, indented and
|
|
172
|
+
* without amounts.
|
|
173
|
+
*/
|
|
174
|
+
const joinWrappedLabels = (rowsArg: IPageRow[]): ILayoutCell[][] => {
|
|
175
|
+
const result: IPageRow[] = [];
|
|
176
|
+
const labelOf = (rowArg: IPageRow): ILayoutCell | undefined =>
|
|
177
|
+
rowArg.cells[0]?.first === 0 && rowArg.cells[0].last === 0 ? rowArg.cells[0] : undefined;
|
|
178
|
+
for (const row of rowsArg) {
|
|
179
|
+
const previous = result[result.length - 1];
|
|
180
|
+
const label = labelOf(row);
|
|
181
|
+
const previousLabel = previous ? labelOf(previous) : undefined;
|
|
182
|
+
if (previous && label && previousLabel && previous.labelX0 !== null && row.labelX0 !== null) {
|
|
183
|
+
const amounts = row.cells.length > 1;
|
|
184
|
+
const previousAmounts = previous.cells.length > 1;
|
|
185
|
+
const coded = splitOutlineCode(previousLabel.text).code !== null;
|
|
186
|
+
if (
|
|
187
|
+
isContinuation(previousLabel.text, label.text) &&
|
|
188
|
+
((amounts && !previousAmounts && coded) || (!amounts && row.labelX0 > previous.labelX0 + 1))
|
|
189
|
+
) {
|
|
190
|
+
previousLabel.text = `${previousLabel.text} ${label.text}`;
|
|
191
|
+
previous.cells.push(...row.cells.slice(1));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
result.push(row);
|
|
196
|
+
}
|
|
197
|
+
return result.map((rowArg) => rowArg.cells);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The blocks of a PDF statement: each page one table. Reads the text layer only; a PDF without one,
|
|
202
|
+
* such as a scan, is `no_text`.
|
|
203
|
+
*/
|
|
204
|
+
export const pdfLayout = async (bytesArg: Uint8Array): Promise<TLayoutBlock[]> => {
|
|
205
|
+
const pdfjs = await plugins.loadPdfjs();
|
|
206
|
+
const packageDirectory = plugins.path.dirname(plugins.createRequire(import.meta.url).resolve('pdfjs-dist/package.json'));
|
|
207
|
+
const loadingTask = pdfjs.getDocument({
|
|
208
|
+
data: new Uint8Array(bytesArg),
|
|
209
|
+
cMapUrl: plugins.path.join(packageDirectory, 'cmaps') + plugins.path.sep,
|
|
210
|
+
cMapPacked: true,
|
|
211
|
+
standardFontDataUrl: plugins.path.join(packageDirectory, 'standard_fonts') + plugins.path.sep,
|
|
212
|
+
useSystemFonts: false,
|
|
213
|
+
disableFontFace: true,
|
|
214
|
+
enableXfa: false,
|
|
215
|
+
verbosity: pdfjs.VerbosityLevel.ERRORS,
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
let pdfDocument: Awaited<typeof loadingTask.promise>;
|
|
219
|
+
try {
|
|
220
|
+
pdfDocument = await loadingTask.promise;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const reason = error instanceof pdfjs.PasswordException ? 'is protected by a password' : 'does not open as a PDF';
|
|
223
|
+
throw new FinancialStatementError('unreadable', `The document ${reason}.`, { cause: error });
|
|
224
|
+
}
|
|
225
|
+
if (pdfDocument.numPages > maxPdfPages) {
|
|
226
|
+
throw new FinancialStatementError('too_large', `The PDF has ${pdfDocument.numPages} pages, more than the ${maxPdfPages} read here.`);
|
|
227
|
+
}
|
|
228
|
+
const blocks: TLayoutBlock[] = [];
|
|
229
|
+
let textFound = false;
|
|
230
|
+
for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber++) {
|
|
231
|
+
const page = await pdfDocument.getPage(pageNumber);
|
|
232
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
233
|
+
const content = await page.getTextContent();
|
|
234
|
+
const pieces: IPiece[] = [];
|
|
235
|
+
for (const item of content.items) {
|
|
236
|
+
if (!('str' in item) || item.str.trim() === '') {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const [a, b, c, d, x, y] = pdfjs.Util.transform(viewport.transform, item.transform) as number[];
|
|
240
|
+
if (Math.abs(b) > 0.01 * Math.abs(a) || Math.abs(c) > 0.01 * Math.abs(d)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const height = item.height > 0 ? item.height : Math.abs(d);
|
|
244
|
+
pieces.push({ text: item.str, x0: x, x1: x + item.width, y, height });
|
|
245
|
+
}
|
|
246
|
+
page.cleanup();
|
|
247
|
+
if (pieces.length === 0) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
textFound = true;
|
|
251
|
+
const rows = pageTable(linesOf(pieces));
|
|
252
|
+
if (rows.length > 0) {
|
|
253
|
+
blocks.push({ kind: 'table', rows });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!textFound) {
|
|
257
|
+
throw new FinancialStatementError('no_text', 'The PDF has no text layer; it may be a scan.');
|
|
258
|
+
}
|
|
259
|
+
return blocks;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof FinancialStatementError) {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
throw new FinancialStatementError('unreadable', 'The PDF could not be read.', { cause: error });
|
|
265
|
+
} finally {
|
|
266
|
+
await loadingTask.destroy();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A statement as this entry point reads it, whatever it came as: text in printed order, and tables
|
|
3
|
+
* of cells. An HTML table keeps its own columns; a PDF page becomes one table whose column 0 holds
|
|
4
|
+
* the labels and whose other columns are the columns its amounts are aligned in.
|
|
5
|
+
*/
|
|
6
|
+
export interface ILayoutCell {
|
|
7
|
+
text: string;
|
|
8
|
+
/** the first column the cell covers */
|
|
9
|
+
first: number;
|
|
10
|
+
/** the last column the cell covers */
|
|
11
|
+
last: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TLayoutBlock =
|
|
15
|
+
| { kind: 'text'; text: string; heading: boolean }
|
|
16
|
+
| { kind: 'table'; rows: ILayoutCell[][] };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TStatementMediaType } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
/** the media types read here, by the names callers and servers give them */
|
|
4
|
+
const mediaTypes: Record<string, TStatementMediaType> = {
|
|
5
|
+
'text/html': 'text/html',
|
|
6
|
+
'application/pdf': 'application/pdf',
|
|
7
|
+
'application/xhtml+xml': 'application/xhtml+xml',
|
|
8
|
+
'application/zip': 'application/zip',
|
|
9
|
+
'application/x-zip-compressed': 'application/zip',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** a media type as read here, its parameters ignored; null for one that is not read */
|
|
13
|
+
export const statementMediaTypeOf = (mediaTypeArg: string): TStatementMediaType | null =>
|
|
14
|
+
mediaTypes[mediaTypeArg.split(';')[0].trim().toLowerCase()] ?? null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outline codes of §§ 266 and 275 HGB as statements print them, from the top level down:
|
|
3
|
+
* capital letters (`A.`), roman numerals (`I.`), arabic numerals (`1.`), small letters (`a)`) and
|
|
4
|
+
* doubled small letters (`aa)`).
|
|
5
|
+
*/
|
|
6
|
+
export interface IOutlineCode {
|
|
7
|
+
/** the code as printed: `A.`, `a)` */
|
|
8
|
+
printed: string;
|
|
9
|
+
/** the code without its punctuation: `A`, `a` */
|
|
10
|
+
key: string;
|
|
11
|
+
/** 0 for capital letters to 4 for doubled small letters */
|
|
12
|
+
rank: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** capital letters stop before I, V and X, which are roman numerals; a code is followed by a space */
|
|
16
|
+
const codePatterns: Array<{ rank: number; pattern: RegExp }> = [
|
|
17
|
+
{ rank: 4, pattern: /^(([a-z])\2)\)(?:\s+|$)/ },
|
|
18
|
+
{ rank: 0, pattern: /^([A-H])[.)](?:\s+|$)/ },
|
|
19
|
+
{ rank: 1, pattern: /^((?=[IVX])X{0,3}(?:IX|IV|V?I{0,3}))\.(?:\s+|$)/ },
|
|
20
|
+
{ rank: 2, pattern: /^(\d{1,2})\.(?:\s+|$)/ },
|
|
21
|
+
{ rank: 3, pattern: /^([a-z])[.)](?:\s+|$)/ },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** splits the outline code off a printed label */
|
|
25
|
+
export const splitOutlineCode = (labelArg: string): { code: IOutlineCode | null; label: string } => {
|
|
26
|
+
for (const { rank, pattern } of codePatterns) {
|
|
27
|
+
const match = pattern.exec(labelArg);
|
|
28
|
+
if (match) {
|
|
29
|
+
return {
|
|
30
|
+
code: { printed: match[0].trim(), key: match[1], rank },
|
|
31
|
+
label: labelArg.slice(match[0].length).trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { code: null, label: labelArg };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** a "davon" line: part of the line above it, never added */
|
|
39
|
+
export const isMemoLabel = (labelArg: string): boolean =>
|
|
40
|
+
/^[-–—•·(]?\s*(?:davon|darunter)\b/i.test(labelArg);
|
|
41
|
+
|
|
42
|
+
/** a line that sums the lines above it: no label at all, or one such as Summe or Bilanzsumme */
|
|
43
|
+
export const isTotalLabel = (labelArg: string): boolean =>
|
|
44
|
+
labelArg === '' || /^(?:Summe|Bilanzsumme|Gesamtsumme|Gesamt|Zwischensumme|Insgesamt)\b/i.test(labelArg);
|