@fin.cx/opendata 4.0.0 → 4.2.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.
Files changed (38) hide show
  1. package/.smartconfig.json +7 -2
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/baserate/baserate.d.ts +9 -0
  4. package/dist_ts/baserate/baserate.js +49 -0
  5. package/dist_ts/baserate/classes.baserateerror.d.ts +14 -0
  6. package/dist_ts/baserate/classes.baserateerror.js +10 -0
  7. package/dist_ts/baserate/data.baserate.d.ts +18 -0
  8. package/dist_ts/baserate/data.baserate.js +63 -0
  9. package/dist_ts/baserate/index.d.ts +4 -0
  10. package/dist_ts/baserate/index.js +7 -0
  11. package/dist_ts/baserate/interfaces.d.ts +19 -0
  12. package/dist_ts/baserate/interfaces.js +2 -0
  13. package/dist_ts/handelsregister/references.d.ts +6 -2
  14. package/dist_ts/handelsregister/references.js +90 -20
  15. package/dist_ts/holidays/calendar.d.ts +32 -0
  16. package/dist_ts/holidays/calendar.js +200 -0
  17. package/dist_ts/holidays/classes.holidaycalendarerror.d.ts +14 -0
  18. package/dist_ts/holidays/classes.holidaycalendarerror.js +10 -0
  19. package/dist_ts/holidays/data.holidays.d.ts +46 -0
  20. package/dist_ts/holidays/data.holidays.js +124 -0
  21. package/dist_ts/holidays/index.d.ts +4 -0
  22. package/dist_ts/holidays/index.js +7 -0
  23. package/dist_ts/holidays/interfaces.d.ts +42 -0
  24. package/dist_ts/holidays/interfaces.js +2 -0
  25. package/package.json +18 -5
  26. package/readme.md +121 -3
  27. package/ts/00_commitinfo_data.ts +2 -2
  28. package/ts/baserate/baserate.ts +57 -0
  29. package/ts/baserate/classes.baserateerror.ts +20 -0
  30. package/ts/baserate/data.baserate.ts +81 -0
  31. package/ts/baserate/index.ts +6 -0
  32. package/ts/baserate/interfaces.ts +21 -0
  33. package/ts/handelsregister/references.ts +94 -20
  34. package/ts/holidays/calendar.ts +261 -0
  35. package/ts/holidays/classes.holidaycalendarerror.ts +20 -0
  36. package/ts/holidays/data.holidays.ts +177 -0
  37. package/ts/holidays/index.ts +18 -0
  38. package/ts/holidays/interfaces.ts +87 -0
package/readme.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @fin.cx/opendata
2
2
 
3
- `@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, and lookups in the German registers through the register portal of the federal states. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
3
+ `@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, the statutory public holidays of the German federal states, and the base rate of § 247 BGB. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
4
4
 
5
- It gives you three practical lanes in one package:
5
+ It gives you five practical lanes in one package:
6
6
 
7
7
  - `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
8
8
  - `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
9
9
  - `HandelsregisterClient`, from `@fin.cx/opendata/handelsregister`, for single lookups in the commercial, cooperative, partnership, association and civil-law partnership registers over plain HTTP, without any storage
10
+ - the holiday calendar, from `@fin.cx/opendata/holidays`, for the statutory public holidays of each German federal state and the working days of § 193 BGB, computed per year without data files or requests
11
+ - the base rate, from `@fin.cx/opendata/baserate`, for the Basiszinssatz of § 247 BGB per half-year as the Deutsche Bundesbank publishes it, for default interest under § 288 BGB
10
12
 
11
13
  ## Issue Reporting and Security
12
14
 
@@ -48,6 +50,26 @@ Everything here comes from `@fin.cx/opendata/handelsregister`. That entry point
48
50
  - `registerCourts`, `findRegisterCourt()`, `parseRegisterReferenceText()`, `formatRegisterReference()`
49
51
  The 150 register courts with their XJustiz codes, and helpers for register references as businesses print them (`Amtsgericht Bremen HRB 35230 HB`).
50
52
 
53
+ ### German Public Holidays
54
+
55
+ Everything here comes from `@fin.cx/opendata/holidays`. That entry point imports nothing else of the package, has no dependencies, and makes no request.
56
+
57
+ - `getPublicHolidays(year, state)`, `getPublicHolidaysOn(date, state)`
58
+ The statutory public holidays of a federal state in a year or on a date, each with its legal basis.
59
+ - `isPublicHoliday()`, `isWorkingDay()`, `nextWorkingDay()`, `workingDayOnOrAfter()`
60
+ Holidays and working days in the sense of § 193 BGB: neither a Saturday, nor a Sunday, nor a statutory public holiday.
61
+ - `easterSunday(year)`, `germanStates`, `findGermanState()`
62
+
63
+ ### The Base Rate Of § 247 BGB
64
+
65
+ Everything here comes from `@fin.cx/opendata/baserate`. That entry point imports nothing else of the package, has no dependencies, and makes no request.
66
+
67
+ - `getBaseRate(date)`
68
+ The base rate in force on a day: its half-year period with the rate in percent a year.
69
+ - `splitByBaseRate(from, to)`
70
+ A span of days split where the rate changes, as interest under § 288 BGB is computed.
71
+ - `baseRatePeriods`, `baseRateFirstDay`, `baseRatesReadOn`, `baseRateSource`
72
+
51
73
  ## Before You Start
52
74
 
53
75
  - No stock or fundamentals providers are pre-registered. You must register them before calling the services.
@@ -312,6 +334,57 @@ import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
312
334
  const content = parseStructuredContent(xmlText);
313
335
  ```
314
336
 
337
+ #### Printed court names
338
+
339
+ `findRegisterCourt()` takes a court's code or its name as the portal or a business prints it: `AG Bremen`, `Amtsgericht Bremen – Registergericht`, `Registergericht Bremen`, `Handelsregister B des Amtsgerichts Bremen`, `Amtsgericht Frankfurt a. M.`, `Frankfurt/O.`, `Kempten` for `Kempten (Allgäu)`, `Freiburg i. Br.`, and `AG Berlin` or `Charlottenburg` for the register court of Berlin. Umlauts may be spelled out (`Muenchen`). It returns a court only when exactly one fits: `Frankfurt` alone could be either Frankfurt am Main or Frankfurt (Oder) and names no court.
340
+
341
+ ### Public Holidays
342
+
343
+ ```ts
344
+ import {
345
+ getPublicHolidays,
346
+ isWorkingDay,
347
+ nextWorkingDay,
348
+ workingDayOnOrAfter,
349
+ } from '@fin.cx/opendata/holidays';
350
+
351
+ getPublicHolidays(2026, 'DE-NI');
352
+ // [{ date: '2026-01-01', id: 'neujahr', name: 'Neujahr', state: 'DE-NI', scope: 'state',
353
+ // legalBasis: '§ 2 Abs. 1 NFeiertagsG' }, …, { date: '2026-10-31', id: 'reformationstag', … }, …]
354
+
355
+ isWorkingDay('2026-06-04', 'DE-HE', { regionalHolidays: 'ignore' }); // false: Fronleichnam
356
+ nextWorkingDay('2026-04-02', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-04-07'
357
+
358
+ // § 193 BGB: a deadline that ends on a Saturday, Sunday or holiday ends on the next working day
359
+ workingDayOnOrAfter('2026-05-14', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-05-15'
360
+ ```
361
+
362
+ States are ISO 3166-2 codes (`DE-BY`); `findGermanState('Bayern')` and `findGermanState('BY')` find one. Dates are plain calendar dates written `YYYY-MM-DD`, so no time zone can move them.
363
+
364
+ #### Regional holidays
365
+
366
+ Four holidays apply only in part of a state:
367
+
368
+ | Holiday | State | Where | Legal basis |
369
+ | --- | --- | --- | --- |
370
+ | Augsburger Friedensfest (8 August) | Bayern | Stadt Augsburg | Art. 1 Abs. 2 FTG |
371
+ | Mariä Himmelfahrt (15 August) | Bayern | municipalities with a predominantly Catholic population, as the Bayerisches Landesamt für Statistik determines them after the last census | Art. 1 Abs. 1 Nr. 2, Abs. 3 FTG |
372
+ | Fronleichnam | Sachsen | the municipalities listed in the annex of the FronleichnamsVO (former districts Bautzen, Hoyerswerda and Kamenz), after a merger only the respective part of the municipality | § 1 Abs. 1 SächsSFG, § 1 FronleichnamsVO |
373
+ | Fronleichnam | Thüringen | the parts of Thüringen where Fronleichnam was a statutory holiday in 1994 | § 2 Abs. 2, § 10 Abs. 1 ThürFGtG |
374
+
375
+ `getPublicHolidays()` lists them with `scope: 'regional'` and the `region` they apply to. The calendar holds no list of these municipalities: the Bavarian list follows the census and the Saxon and Thuringian ones name parts of municipalities, so a place cannot be matched to them reliably by a key. Every working-day question therefore says how to treat them:
376
+
377
+ - `{ regionalHolidays: 'count' }` treats them as holidays. A period then never ends on a day that is a holiday at the place in question, at the price of ending one day later where it is not. Use it when the place is unknown.
378
+ - `{ regionalHolidays: 'ignore' }` treats them as working days, when the place is known to lie outside that part.
379
+
380
+ #### What the calendar covers
381
+
382
+ - The statutory public holidays of the 16 federal states from 2018 on, and 3 October from Art. 2 Abs. 2 of the Unification Treaty. Every change since 2018 applies from its year: Reformationstag in Bremen, Hamburg, Niedersachsen and Schleswig-Holstein (2018), Frauentag in Berlin (2019) and Mecklenburg-Vorpommern (2023), Weltkindertag in Thüringen (2019), and the one-off holidays of Berlin on 8 May 2020, 8 May 2025 and 17 June 2028. Years before 2018 end in `HolidayCalendarError` with the code `unsupported_year`.
383
+ - Later years follow the laws as they stood when this version read them (`holidayLawsReadOn`). Several states allow their government to declare one-off holidays by ordinance; one declared after that day needs a new version.
384
+ - The Easter-based holidays follow Easter Sunday of the Gregorian calendar; Buß- und Bettag (Sachsen) is the Wednesday before the last Sunday before the first Sunday of Advent.
385
+ - The laws of Berlin and Hessen also count every Sunday as a public holiday, and Brandenburg names Ostersonntag and Pfingstsonntag. The calendar lists the Sundays Brandenburg names; for working days every Sunday is excluded anyway.
386
+ - Religious holidays that protect only church services or give employees time off (for example Fronleichnam outside its region in Sachsen, § 3 SächsSFG) and memorial days (Gedenktage) are no statutory public holidays and are not listed.
387
+
315
388
  ## API At A Glance
316
389
 
317
390
  ### `StockPriceService`
@@ -375,11 +448,31 @@ const content = parseStructuredContent(xmlText);
375
448
  - `formatRegisterReference(reference)`
376
449
  - `structuredContentNotice`
377
450
 
451
+ ### Holiday calendar (`@fin.cx/opendata/holidays`)
452
+
453
+ - `getPublicHolidays(year, state)`
454
+ - `getPublicHolidaysOn(date, state)`
455
+ - `isPublicHoliday(date, state, { regionalHolidays })`
456
+ - `isWorkingDay(date, state, { regionalHolidays })`
457
+ - `nextWorkingDay(date, state, { regionalHolidays })`
458
+ - `workingDayOnOrAfter(date, state, { regionalHolidays })`
459
+ - `easterSunday(year)`
460
+ - `germanStates`, `findGermanState(codeOrName)`
461
+ - `holidayCalendarFirstYear`, `holidayLawsReadOn`
462
+ - `HolidayCalendarError` with the codes `invalid_date`, `unsupported_year`, `unknown_state` and `invalid_options`
463
+
464
+ ### Base rate (`@fin.cx/opendata/baserate`)
465
+
466
+ - `getBaseRate(date)`
467
+ - `splitByBaseRate(from, to)`
468
+ - `baseRatePeriods`, `baseRateFirstDay`, `baseRatesReadOn`, `baseRateSource`
469
+ - `BaseRateError` with the codes `invalid_date`, `invalid_range`, `before_first_period` and `not_published`
470
+
378
471
  ## Data Sources
379
472
 
380
473
  The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time.
381
474
 
382
- The only data the package carries are the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). They are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
475
+ The package carries two kinds of data: the holiday rules of `@fin.cx/opendata/holidays` (`ts/holidays/data.holidays.ts`), described at the end of this section, and the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). The code tables are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
383
476
 
384
477
  | Table | Code list | Issued by |
385
478
  | --- | --- | --- |
@@ -399,6 +492,31 @@ Codes and designations are reproduced as published, without changes. The tables
399
492
 
400
493
  A later version of a code list may add codes. The parser keeps a code it does not know as a code, without a designation, and never guesses one.
401
494
 
495
+ The holiday calendar is derived from the holiday laws of the federal states, read on 2026-09-24 in their official portals, from Art. 2 Abs. 2 of the Unification Treaty for 3 October, and it answers the question of § 193 BGB:
496
+
497
+ | State | Law | Provision |
498
+ | --- | --- | --- |
499
+ | all | [Einigungsvertrag](https://www.gesetze-im-internet.de/einigvtr/art_2.html) | Art. 2 Abs. 2 (Tag der Deutschen Einheit) |
500
+ | Baden-Württemberg | [Feiertagsgesetz (FTG)](https://www.landesrecht-bw.de/perma?j=FeiertG_BW_!_1) | § 1 |
501
+ | Bayern | [Feiertagsgesetz (FTG)](https://www.gesetze-bayern.de/Content/Document/BayFTG-1) | Art. 1 |
502
+ | Berlin | [Gesetz über die Sonn- und Feiertage](https://gesetze.berlin.de/perma?j=FeiertG_BE_!_1) | § 1 Abs. 1 |
503
+ | Brandenburg | [Feiertagsgesetz (FTG)](https://bravors.brandenburg.de/gesetze/ftg_2015) | § 2 Abs. 1 |
504
+ | Bremen | [Gesetz über die Sonn-, Gedenk- und Feiertage](https://www.transparenz.bremen.de/sixcms/detail.php?gsid=bremen2014_tp.c.145882.de) | § 2 Abs. 1 |
505
+ | Hamburg | [Feiertagsgesetz](https://www.landesrecht-hamburg.de/perma?d=jlr-NNLHH00006CB9NN00000000002) | § 1 |
506
+ | Hessen | [Hessisches Feiertagsgesetz (HFeiertagsG)](https://www.rv.hessenrecht.hessen.de/perma?j=FeiertG_HE_!_1) | § 1 Abs. 1 |
507
+ | Mecklenburg-Vorpommern | [Feiertagsgesetz (FTG M-V)](https://www.landesrecht-mv.de/perma?d=jlr-NNLMV00007A2FNN00000000003) | § 2 Abs. 1 |
508
+ | Niedersachsen | [Niedersächsisches Gesetz über die Feiertage (NFeiertagsG)](https://voris.wolterskluwer-online.de/browse/document/f74bc6e7-6ded-3c2c-9afb-b34620456e56) | § 2 Abs. 1 |
509
+ | Nordrhein-Westfalen | [Gesetz über die Sonn- und Feiertage (Feiertagsgesetz NW)](https://recht.nrw.de/lrgv/gesetz/01012000-bekanntmachung-der-neufassung-des-gesetzes-ueber-die-sonn-und-feiertage) | § 2 Abs. 1 |
510
+ | Rheinland-Pfalz | [Feiertagsgesetz (LFtG)](https://landesrecht.rlp.de/perma?d=jlr-NNLRP00006125NN00000000004) | § 2 Abs. 1 |
511
+ | Saarland | [Feiertagsgesetz (SFG)](https://recht.saarland.de/perma?d=jlr-NNLSL0000A2D6NN00000000007) | § 2 Abs. 1 |
512
+ | Sachsen | [Sächsisches Sonn- und Feiertagsgesetz (SächsSFG)](https://www.revosax.sachsen.de/vorschrift/3997-SaechsSFG), [FronleichnamsVO](https://www.revosax.sachsen.de/vorschrift/4142) | § 1 Abs. 1; § 1 and annex |
513
+ | Sachsen-Anhalt | [Gesetz über die Sonn- und Feiertage (FeiertG LSA)](https://www.landesrecht.sachsen-anhalt.de/perma?d=jlr-NNLST0000411CNN00000000003) | § 2 |
514
+ | Schleswig-Holstein | [Gesetz über Sonn- und Feiertage (SFTG)](https://www.gesetze-rechtsprechung.sh.juris.de/perma?j=FeiertG_SH_!_2) | § 2 Abs. 1 |
515
+ | Thüringen | [Thüringer Feier- und Gedenktagsgesetz (ThürFGtG)](https://landesrecht.thueringen.de/perma?j=FeiertG_TH_!_2) | § 2, § 10 Abs. 1 |
516
+ | all | [Bürgerliches Gesetzbuch](https://www.gesetze-im-internet.de/bgb/__193.html) | § 193 (Sonntag, Feiertag, Sonnabend) |
517
+
518
+ The base rate of `@fin.cx/opendata/baserate` (`ts/baserate/data.baserate.ts`) is the table "Basiszinssatz nach § 247 BGB" of the Deutsche Bundesbank (https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820), which the Bundesbank publishes under § 247 Abs. 2 BGB, read on 2026-09-27 and reproduced unaltered: every half-year from 1 July 2002, the first row it lists, to the half-year from 1 July 2026 (1,52 %). The rate changes on 1 January and 1 July (§ 247 Abs. 1 Satz 2 BGB); a day after 31 December 2026 answers `not_published` until a release carries the next row.
519
+
402
520
  ## Behavior And Caveats
403
521
 
404
522
  - Historical market data is cached aggressively because it is treated as immutable.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '4.0.0',
7
- description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, and stock and crypto prices and fundamentals from several providers.'
6
+ version: '4.2.0',
7
+ description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, and stock and crypto prices and fundamentals from several providers.'
8
8
  }
@@ -0,0 +1,57 @@
1
+ import { BaseRateError } from './classes.baserateerror.js';
2
+ import { baseRateFirstDay, baseRatePeriods, baseRatesReadOn } from './data.baserate.js';
3
+ import type { IBaseRatePeriod, IBaseRateSpan, TIsoDate } from './interfaces.js';
4
+
5
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
6
+
7
+ const checkDate = (dateArg: TIsoDate): void => {
8
+ const match = typeof dateArg === 'string' ? ISO_DATE.exec(dateArg) : null;
9
+ const utc = match ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null;
10
+ if (!match || !utc || utc.toISOString().slice(0, 10) !== dateArg) {
11
+ throw new BaseRateError('invalid_date', `${JSON.stringify(dateArg)} is not a calendar date written YYYY-MM-DD.`);
12
+ }
13
+ };
14
+
15
+ const addDay = (dateArg: TIsoDate): TIsoDate => {
16
+ const utc = new Date(`${dateArg}T00:00:00Z`);
17
+ utc.setUTCDate(utc.getUTCDate() + 1);
18
+ return utc.toISOString().slice(0, 10);
19
+ };
20
+
21
+ /** The base rate in force on a day: the published period that contains it. */
22
+ export const getBaseRate = (dateArg: TIsoDate): IBaseRatePeriod => {
23
+ checkDate(dateArg);
24
+ if (dateArg < baseRateFirstDay) {
25
+ throw new BaseRateError('before_first_period', `${dateArg} lies before ${baseRateFirstDay}, the first day of the Bundesbank's table.`);
26
+ }
27
+ const period = baseRatePeriods.find((periodArg) => periodArg.validFrom <= dateArg && dateArg <= periodArg.validTo);
28
+ if (!period) {
29
+ throw new BaseRateError(
30
+ 'not_published',
31
+ `${dateArg} lies after ${baseRatePeriods[baseRatePeriods.length - 1].validTo}; the base rate of its half-year was not published when the table was read on ${baseRatesReadOn}.`,
32
+ );
33
+ }
34
+ return period;
35
+ };
36
+
37
+ /**
38
+ * A span of days, both inclusive, split where the base rate changes: one piece per published
39
+ * period it touches, oldest first. Interest under § 288 BGB is computed per piece, since the rate
40
+ * of each half-year applies to the days in it.
41
+ */
42
+ export const splitByBaseRate = (fromArg: TIsoDate, toArg: TIsoDate): IBaseRateSpan[] => {
43
+ checkDate(fromArg);
44
+ checkDate(toArg);
45
+ if (toArg < fromArg) {
46
+ throw new BaseRateError('invalid_range', `${toArg} lies before ${fromArg}.`);
47
+ }
48
+ const spans: IBaseRateSpan[] = [];
49
+ let day = fromArg;
50
+ while (day <= toArg) {
51
+ const period = getBaseRate(day);
52
+ const to = period.validTo < toArg ? period.validTo : toArg;
53
+ spans.push({ from: day, to, ratePercent: period.ratePercent });
54
+ day = addDay(to);
55
+ }
56
+ return spans;
57
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Why a question to the base-rate table was refused:
3
+ * - `invalid_date`: the value is not a calendar date written `YYYY-MM-DD`;
4
+ * - `invalid_range`: a span ends before it begins;
5
+ * - `before_first_period`: the day lies before the first period the table holds (1 July 2002);
6
+ * - `not_published`: the day lies after the half-year of the last period the table holds, whose
7
+ * rate the Bundesbank had not published when the table was read (`baseRatesReadOn`).
8
+ */
9
+ export type TBaseRateErrorCode = 'invalid_date' | 'invalid_range' | 'before_first_period' | 'not_published';
10
+
11
+ /** The typed error of the base-rate table. */
12
+ export class BaseRateError extends Error {
13
+ public readonly code: TBaseRateErrorCode;
14
+
15
+ constructor(code: TBaseRateErrorCode, message: string) {
16
+ super(message);
17
+ this.name = 'BaseRateError';
18
+ this.code = code;
19
+ }
20
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The base rate (Basiszinssatz) of § 247 BGB, as the Deutsche Bundesbank publishes it: "Gemäß
3
+ * § 247 Abs. 2 BGB ist die Deutsche Bundesbank verpflichtet, den aktuellen Stand des
4
+ * Basiszinssatzes im Bundesanzeiger zu veröffentlichen." It changes on 1 January and 1 July
5
+ * (§ 247 Abs. 1 Satz 2 BGB). The table below is the Bundesbank's table "Basiszinssatz nach § 247
6
+ * BGB" (https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820),
7
+ * read on 2026-09-27, unaltered: every row from 1 July 2002, the first one it lists, to the
8
+ * current one. A later half-year needs a new release.
9
+ */
10
+ import type { IBaseRatePeriod } from './interfaces.js';
11
+
12
+ /** the first day the table answers for */
13
+ export const baseRateFirstDay = '2002-07-01';
14
+
15
+ /** the day the Bundesbank's table was read; later half-years need a new release */
16
+ export const baseRatesReadOn = '2026-09-27';
17
+
18
+ /** the page the table was read from */
19
+ export const baseRateSource = 'https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820';
20
+
21
+ const rows: ReadonlyArray<readonly [string, number]> = [
22
+ // gültig ab, Basiszinssatz in Prozent, as the table of the Deutsche Bundesbank states them
23
+ ['2002-07-01', 2.47],
24
+ ['2003-01-01', 1.97],
25
+ ['2003-07-01', 1.22],
26
+ ['2004-01-01', 1.14],
27
+ ['2004-07-01', 1.13],
28
+ ['2005-01-01', 1.21],
29
+ ['2005-07-01', 1.17],
30
+ ['2006-01-01', 1.37],
31
+ ['2006-07-01', 1.95],
32
+ ['2007-01-01', 2.70],
33
+ ['2007-07-01', 3.19],
34
+ ['2008-01-01', 3.32],
35
+ ['2008-07-01', 3.19],
36
+ ['2009-01-01', 1.62],
37
+ ['2009-07-01', 0.12],
38
+ ['2010-01-01', 0.12],
39
+ ['2010-07-01', 0.12],
40
+ ['2011-01-01', 0.12],
41
+ ['2011-07-01', 0.37],
42
+ ['2012-01-01', 0.12],
43
+ ['2012-07-01', 0.12],
44
+ ['2013-01-01', -0.13],
45
+ ['2013-07-01', -0.38],
46
+ ['2014-01-01', -0.63],
47
+ ['2014-07-01', -0.73],
48
+ ['2015-01-01', -0.83],
49
+ ['2015-07-01', -0.83],
50
+ ['2016-01-01', -0.83],
51
+ ['2016-07-01', -0.88],
52
+ ['2017-01-01', -0.88],
53
+ ['2017-07-01', -0.88],
54
+ ['2018-01-01', -0.88],
55
+ ['2018-07-01', -0.88],
56
+ ['2019-01-01', -0.88],
57
+ ['2019-07-01', -0.88],
58
+ ['2020-01-01', -0.88],
59
+ ['2020-07-01', -0.88],
60
+ ['2021-01-01', -0.88],
61
+ ['2021-07-01', -0.88],
62
+ ['2022-01-01', -0.88],
63
+ ['2022-07-01', -0.88],
64
+ ['2023-01-01', 1.62],
65
+ ['2023-07-01', 3.12],
66
+ ['2024-01-01', 3.62],
67
+ ['2024-07-01', 3.37],
68
+ ['2025-01-01', 2.27],
69
+ ['2025-07-01', 1.27],
70
+ ['2026-01-01', 1.27],
71
+ ['2026-07-01', 1.52],
72
+ ];
73
+
74
+ /** the last day of the half-year a period begins in */
75
+ const halfYearEndOf = (validFromArg: string): string =>
76
+ `${validFromArg.slice(0, 4)}-${validFromArg.slice(5, 7) === '01' ? '06-30' : '12-31'}`;
77
+
78
+ /** every published period, oldest first */
79
+ export const baseRatePeriods: readonly IBaseRatePeriod[] = Object.freeze(
80
+ rows.map(([validFrom, ratePercent]) => Object.freeze({ validFrom, validTo: halfYearEndOf(validFrom), ratePercent })),
81
+ );
@@ -0,0 +1,6 @@
1
+ // This entry point stands alone: it imports nothing from the rest of the package, has no
2
+ // dependencies and needs no storage or network.
3
+ export * from './interfaces.js';
4
+ export { BaseRateError, type TBaseRateErrorCode } from './classes.baserateerror.js';
5
+ export { baseRateFirstDay, baseRatePeriods, baseRateSource, baseRatesReadOn } from './data.baserate.js';
6
+ export { getBaseRate, splitByBaseRate } from './baserate.js';
@@ -0,0 +1,21 @@
1
+ /** A plain calendar date, `YYYY-MM-DD`. */
2
+ export type TIsoDate = string;
3
+
4
+ /**
5
+ * One half-year's base rate (Basiszinssatz, § 247 BGB) as the Deutsche
6
+ * Bundesbank publishes it: valid from 1 January or 1 July to the last day of
7
+ * that half-year, both inclusive.
8
+ */
9
+ export interface IBaseRatePeriod {
10
+ validFrom: TIsoDate;
11
+ validTo: TIsoDate;
12
+ /** percent a year; it can be negative (2013 to 2022) */
13
+ ratePercent: number;
14
+ }
15
+
16
+ /** A span of days within one base-rate period, both days inclusive. */
17
+ export interface IBaseRateSpan {
18
+ from: TIsoDate;
19
+ to: TIsoDate;
20
+ ratePercent: number;
21
+ }
@@ -21,25 +21,103 @@ export interface IParsedRegisterReferenceText {
21
21
 
22
22
  const collapse = (textArg: string): string => textArg.replace(/\s+/g, ' ').trim();
23
23
 
24
- const courtKey = (textArg: string): string =>
25
- collapse(textArg)
26
- .replace(/^(?:amtsgericht|registergericht|ag)\s+/i, '')
27
- .toLocaleLowerCase('de-DE');
24
+ /** German letters as a text without them spells them: `Muenchen` is `München` */
25
+ const spelledOut: Readonly<Record<string, string>> = { 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss' };
28
26
 
29
- /** the alternative name the portal puts in brackets: `Berlin (Charlottenburg)` → `charlottenburg` */
30
- const courtAliasKey = (nameArg: string): string | undefined => {
31
- const bracket = /\(([^()]+)\)\s*$/.exec(nameArg);
32
- return bracket ? courtKey(bracket[1]) : undefined;
27
+ /** words that name a court or a register rather than its place */
28
+ const courtWords: ReadonlySet<string> = new Set([
29
+ 'amtsgericht',
30
+ 'amtsgerichts',
31
+ 'ag',
32
+ 'registergericht',
33
+ 'registergerichts',
34
+ 'handelsregister',
35
+ 'genossenschaftsregister',
36
+ 'partnerschaftsregister',
37
+ 'vereinsregister',
38
+ 'gesellschaftsregister',
39
+ 'abteilung',
40
+ 'beim',
41
+ 'bei',
42
+ 'des',
43
+ ]);
44
+
45
+ /**
46
+ * The place a printed court name names, as courts are compared: lower case, German letters spelled
47
+ * out, punctuation as spaces, and without the words that name a court or a register or the
48
+ * division of the commercial register. `Handelsregister B des Amtsgerichts Frankfurt a. M.` is
49
+ * `frankfurt a m`.
50
+ */
51
+ const placeKey = (textArg: string): string => {
52
+ const words = textArg
53
+ .normalize('NFC')
54
+ .toLocaleLowerCase('de-DE')
55
+ .replace(/[äöüß]/g, (letterArg) => spelledOut[letterArg] ?? letterArg)
56
+ .split(/[^\p{L}\p{N}]+/u)
57
+ .filter((wordArg) => wordArg !== '');
58
+ return words
59
+ .filter((wordArg, indexArg) => {
60
+ const previous = words[indexArg - 1];
61
+ const division = (previous === 'handelsregister' || previous === 'abteilung') && (wordArg === 'a' || wordArg === 'b');
62
+ return !courtWords.has(wordArg) && !division;
63
+ })
64
+ .join(' ');
33
65
  };
34
66
 
35
67
  /**
36
- * Finds a register court by its code or its name: `H1101`, `Bremen`, `Amtsgericht Bremen`,
37
- * `Berlin (Charlottenburg)` or `Charlottenburg`. Returns undefined unless exactly one court fits.
68
+ * Other names under which the register courts are printed, for courts whose name in the official
69
+ * list carries a qualifier that prints leave out, abbreviate or spell out. Each name here is a name
70
+ * of that court alone. `Frankfurt` without `am Main` or `(Oder)` is a name of neither of the two.
71
+ */
72
+ const printedCourtNames: Readonly<Record<string, readonly string[]>> = {
73
+ 'Bad Homburg v.d.H.': ['Bad Homburg', 'Bad Homburg vor der Höhe', 'Bad Homburg v. d. Höhe'],
74
+ 'Berlin (Charlottenburg)': ['Berlin', 'Charlottenburg', 'Charlottenburg (Berlin)'],
75
+ 'Frankfurt am Main': ['Frankfurt a. M.', 'Frankfurt a. Main', 'Frankfurt/M.', 'Frankfurt/Main', 'Frankfurt (Main)'],
76
+ 'Frankfurt (Oder)': ['Frankfurt/O.', 'Frankfurt a. d. O.', 'Frankfurt an der Oder', 'Frankfurt a. d. Oder'],
77
+ 'Freiburg': ['Freiburg im Breisgau', 'Freiburg i. Br.', 'Freiburg/Breisgau', 'Freiburg (Breisgau)'],
78
+ 'Kempten (Allgäu)': ['Kempten'],
79
+ 'Königstein': ['Königstein im Taunus', 'Königstein i. Ts.'],
80
+ 'Landau': ['Landau in der Pfalz', 'Landau i. d. Pfalz', 'Landau/Pfalz'],
81
+ 'Limburg': ['Limburg an der Lahn', 'Limburg a. d. Lahn'],
82
+ 'Ludwigshafen a.Rhein (Ludwigshafen)': ['Ludwigshafen', 'Ludwigshafen am Rhein'],
83
+ 'Offenbach am Main': ['Offenbach', 'Offenbach a. M.'],
84
+ 'Oldenburg (Oldenburg)': ['Oldenburg', 'Oldenburg (Oldb.)', 'Oldenburg i. O.'],
85
+ 'St. Ingbert (St Ingbert)': ['St. Ingbert', 'Sankt Ingbert'],
86
+ 'St. Wendel (St Wendel)': ['St. Wendel', 'Sankt Wendel'],
87
+ 'Weiden i. d. OPf.': ['Weiden', 'Weiden in der Oberpfalz'],
88
+ };
89
+
90
+ /** every place key of a list of courts with the courts it names */
91
+ const courtsByPlaceKey = (courtsArg: readonly IRegisterCourt[]): ReadonlyMap<string, readonly IRegisterCourt[]> => {
92
+ const byKey = new Map<string, IRegisterCourt[]>();
93
+ for (const court of courtsArg) {
94
+ const keys = new Set([court.name, ...(printedCourtNames[court.name] ?? [])].map(placeKey));
95
+ for (const key of keys) {
96
+ if (key !== '') {
97
+ byKey.set(key, [...(byKey.get(key) ?? []), court]);
98
+ }
99
+ }
100
+ }
101
+ return byKey;
102
+ };
103
+
104
+ let officialCourtKeys: ReadonlyMap<string, readonly IRegisterCourt[]> | undefined;
105
+
106
+ /**
107
+ * Finds a register court by its code or by its name as the portal or a business prints it: `H1101`,
108
+ * `Bremen`, `AG Bremen`, `Amtsgericht Bremen – Registergericht`, `Registergericht Bremen`,
109
+ * `Handelsregister B des Amtsgerichts Bremen`, `Amtsgericht Frankfurt a. M.`, `Frankfurt (Oder)`,
110
+ * `Kempten (Allgäu)` or `Kempten`, `Freiburg i. Br.`, `AG Berlin` or `Charlottenburg`. Umlauts may
111
+ * be spelled out (`Muenchen`). Returns undefined unless exactly one court fits: a place the list
112
+ * does not know, and `Frankfurt` alone, name no court.
38
113
  */
39
114
  export const findRegisterCourt = (
40
115
  codeOrNameArg: string,
41
116
  courtsArg: readonly IRegisterCourt[] = registerCourts
42
117
  ): IRegisterCourt | undefined => {
118
+ if (typeof codeOrNameArg !== 'string') {
119
+ return undefined;
120
+ }
43
121
  const text = collapse(codeOrNameArg);
44
122
  if (!text) {
45
123
  return undefined;
@@ -48,16 +126,12 @@ export const findRegisterCourt = (
48
126
  if (byCode) {
49
127
  return byCode;
50
128
  }
51
- const key = courtKey(text);
52
- const byName = courtsArg.filter((courtArg) => courtKey(courtArg.name) === key);
53
- if (byName.length === 1) {
54
- return byName[0];
55
- }
56
- if (byName.length > 1) {
57
- return undefined;
58
- }
59
- const byAlias = courtsArg.filter((courtArg) => courtAliasKey(courtArg.name) === key);
60
- return byAlias.length === 1 ? byAlias[0] : undefined;
129
+ const keys =
130
+ courtsArg === registerCourts
131
+ ? (officialCourtKeys ??= courtsByPlaceKey(registerCourts))
132
+ : courtsByPlaceKey(courtsArg);
133
+ const courts = keys.get(placeKey(text)) ?? [];
134
+ return courts.length === 1 ? courts[0] : undefined;
61
135
  };
62
136
 
63
137
  /** returns the canonical register type for `hrb`, `HRB`, `gnr` …, or undefined */