Stammbäume für Entwickler

„Stammbäume sind schon komisch, alle Bäume haben doch einem Stamm“ ― Alicia

Stammbäume sind für viele Menschen ein spannendes Steckenpferd, egal ob Fachmann oder Laie, ob Historiker oder Nostalgiker, ob Jung oder Alt, jeder freut sich über den Anblick der eigenen Ahnenreihe. Bei dem einen reicht sie weit in die Vergangenheit, bei dem anderen finden sich bloß Eltern und Großeltern.

Abseits der Sammelwut, die auch Schmetterlingsfänger mit ihren Schaukästen auszeichnet, gibt es auch für Informatiker viele interessante Themen, die sich um den Stammbaum ranken.

Das Thema wurde für mich als Software Entwickler wieder spannend, als meine Töchter endlich einmal den Stammbaum der ganzen Familie ausgedruckt sehen wollten. Leider beherrscht, dass von mir verwendete Proramm nur 3 Generationen in der Druckansicht und Männer ubd Frauen wurden zum Unwillen der Töchter auch nicht farblich unterschieden.

Beginnen wollen wir mit GEDCOM, dem bekanntesten Austauschformat für genealogische Daten.  Die aktuelle Version 5.5 existiert schon seit 1999 und basiert auf Textdateien, in denen die Zeilen einzelne Datensätze darstellen.  Jeder Datensatz hat die Form

<level>  [<reference>] <tag> [<value>]

Über den Wert Level wird die  hierarchische Ebene des einzelnen Datensatz beschrieben, wobei der Wert 0 die oberste Ebene definiert. Komplexe Strukturen werden über viele verschiedene Datensätze zusammengestellt. Der Typ des Datensatzes wird über das Tag bestimmt und sein optionaler Wert über den Value definiert. Um auf einen Datensatz zu verweisen, kann die optionale Referenz verwendet werden.

Von den etwa 130 Tags aus der Spezifikation werden wir uns hier mit etwa einem Dutzend beschäftigen, insbesondere INDI und FAM.

Das Tag INDI dient zur Beschreibung von einzelnen Personen. Es steht auf oberster Ebene einer komplexen Struktur von Datensätzen, die alle bekannten Informationen zu Person beschreiben.  Das folgende Beispiel aus insgesamt 13 Datensätzen definiert den Eintrag für meinen Ahnen Johann Seemann, der am 6. September 1781 in Kirchhuchting geboren wurde und eine Woche später auch dort getauft wurde. Er arbeitete 1815 als Bierbrauer in Bremen.

0 @P30@ INDI 
1 NAME Johann /Seemann/
1 BIRT 
2 DATE 06 Sep 1781
2 PLAC Kirchhuchting
1 BAPM 
2 DATE 13 Sep 1781
2 PLAC Kirchhuchting
1 SEX M
1 OCCU Bierbrauer
2 DATE 1815
2 PLAC Bremen, Deutschland
1 FAMS @F10@

Betrachten wir das Beispiel etwas genauer, dann sehen wir in der ersten Zeile den einleitenden Tag INDI in der obersten Ebene 0. Vor dem Tag ist eine Referenz @P30@ für die Person. Mit dieser Referenz kann an anderer Stelle der Datei auf Johann Seemann verwiesen werden. In der nächsten Ebene 1 werden der Name (NAME), das Geschlecht (SEX), und die Ereignisse Geburt (BIRT), Taufe (BAPM) und Tätigkeit (OCCU) definiert. Die Ereignisse sind ihrerseits Strukturen und bestehen aus einem Ort (PLAC) und einer Zeitangabe (DATE) auf der Ebene 2. In der letzten Zeile wird auf eine Familie verwiesen, in der er Elternteil (FAMS) ist. Da kein Familie aufgeführt ist, in der er als Kind (FAMC) geführt wird, ist er eines der Blätter an meinem Stammbaum.

Für die meisten von uns sind Format wie XML, JSON oder YAML geläufiger, daher dasselbe Beispiel einmal in YAML Notation.

INDI: 
  REF: P30
  SEX: M
  NAME: Johann /Seemann/
  BIRT:
    DATE: 1781-09-06
    PLACE: Kirchhuchting
  BAPM:
    DATE: 1781-09-13
    PLACE: Kirchhuchting
  OCCU:
    DATE: 1815
    PLACE: Bremen
  FAMS:
    REF: F10

In der YAML Variante ist die hierarchische Gliederung der Datensätze optisch sehr viel besser zu erfassen. Aus diesen Grund folgt das Beispiel für das Tag FAM hier auch direkt in der YAML Notation.

FAM:
  REF: F10
  HUSB: P30
  WIFE: P31
  CHIL: P29

Dieses Beispiel wiederum ist recht kurz und geht nur über 4 Zeilen.  Von dieser Familie ist nur bekannt, dass sie aus Ehemann (HUSB), Ehefrau (WIFE) und einem Kind (CHIL) besteht. Dies ist auch eine der Besonderheiten, die man beim Arbeiten mit GEDCOM Dateien beachten muss. Viele Informationen sind, der Historie geschuldet, ungenau, mehrdeutig oder nicht verfügbar. Sind sich verschiedene Quellen nicht einig darüber, wann einer der Vorfahren geboren wurde, dann können mehrere Ereignisse vom Typ BIRT für die Person angegeben werden, jede mit eigener Quellenangabe.

Das Parsen von GEDCOM Dateien mit Java ist trivial. Wir lesen eine Zeile, zerlegen sie und erstellen ein Token, das die Informationen zum Datensatz enthält.

public class Token {
  public static Token byLine(String line) {
    String[] parts = line.split(" ");
    int level = Integer.parseInt(parts[0]);Optional<String> reference = getReference(parts[1]);
    Tag tag = Tag.valueOf(reference.isPresent()) ? parts[2] : parts[3]);
    Optional<String> value = reference.isPresent() ? parts[3] : parts[4]);
    return new Token(level, tag, reference, value);
  }
  private Token(int level, Tag tag, Optional<String> reference, Optional<String> value) { ... }
  ...
}

Dann durchlaufen wir den Token Stream und prüfen Elemente auf Ebene 0 auf den Type INDI. Finden wir einen solchen Datensatz, dann nutzen wir alle folgenden Datensätze um ein POJO Person zu befüllen, bis wir wieder auf einen Datensatz auf Ebene 0 stoßen.

public class Person {
  public Person(String Name, Gender gender, Family Family, Optional<Event> birth, Optional<Event> death) { ... }
  ...
}

Jede gefundene Person wird in eine Liste eingetragen und für die Familienreferenzen in FAMS und FAMC, die entsprechenden Familien geholt und befüllt.

public class Family {
  List<Person> children;
  Optional<Person> husband;
  Optional<Person> wife;
  ...
}

Bei FAMC wird die Person in die Liste der Kinder, bei FAMS als Elternteil eingetragen. Warum die Familienzugehörigkeit, bei den Personen- und zusätzlich bei den Familieneinträgen stehen verwundert ein wenig. Ausreichend wäre die Information in den Familieneinträgen gewesen.

Die extrahierten Personen und Familien können nun für alle möglichen Dinge verwendet werden. Die Häufigkeit der Vor- und Nachnamen bestimmen, die durchschnittliche Lebenserwartung und Kindersterblichkeit berechnen oder natürlich, um einen Stammbaum zu zeichnen.

Hier geben wir einfach mal die Namen aller Familienmitglieder aus.

persons.stream().forEach(p -> System.out.printeln(Person.getName());

In der aktuellen Version entspricht die Implementierung den Vorgaben meiner GEDCOM Datei und nicht dem vollständigen Standard. Bis zu einer Veröffentlichung des Quellcodes werden aber die gröbsten Unzulänglichkeiten ausgemerzt.