GEDCOM im Detail

Der Zauber steckt immer im Detail.“ – Theodor Fontane

Im vorherigen Beitrag Stammbäume für Entwickler wurde grob skizziert, wie GEDCOM Dateien mit einem einfachen Java Programm eingelesen werden können.

Hier werden wir einige Details der Spezifikation genauer betrachten und zeigen, wie sie in Java leicht umgesetzt werden können.

Das Einlesen der Informationen geschah bislang nur über die Personen und nicht über die Familien. Dadurch gehen uns aber etwaige Informationen verloren, die nur an der Familienstruktur hängen. Wie etwa das Heirat Ereignis (MARR)  im folgenden fiktiven Beispiel in YAML Notation.

FAM: 
  REF: F1
  HUS: 
    REF: P1
  MARR: 
    DATE: 1998

Wir ergänzen also unsere Routine zum Einlesen der Daten, indem wir für jede FAM Struktur, die enthaltenen Referenzen mit denen der INDI Strukturen abgleichen und zusätzliche Familienereignisse hinzufügen. Warum dieses Austauschformat, die Familienrelationen über zwei Strukturen verteilt, darüber kann man wohl nur spekulieren.

Das jede Zeile ein eigener Datensatz ist, war eine kleine didaktische Lüge, denn eine Zeile kann in diesem Standard nur 255 Zeichen lang sein. Für lange Texte haben die Entwickler des Standards die Tags CONT und CONC bereitgestellt, deren Werte den vorhergehenden Tag angehängt werden. Dabei wird bei CONT ein Zeilenumbruch eingefügt und bei CONC nicht.

2 DESC Dies ist ein Text
3 CONT und noch mehr Text

Diese Besonderheit behandeln wir innerhalb des TokenReaders, und liefern in diesem Fall nur das DESC Element mit dem endgültigen Wert „Dies ist ein Text und noch mehr Text“.

Im Header der GEDCOM Datei werden eine Reihe von Metadaten geliefert, u.a. die Version des Formats, das verwendete Programm, die Zeichensatzkodierung, die Sprache und einige andere Dinge. Der Vollständigkeit halber lesen wir auch diese Daten ein.

0 HEAD
1 SOUR Anchestor Archive
2 VERS 1.0
2 NAME Anchestor Archive 
2 CORP ACME, Inc.
3 PHON (555) 100-1000
1 DEST Anchestor Archive
1 DATE 24 AUG 2018
1 CHAR UTF-8
1 FILE C:\Users\Jens\Desktop\kaiser.GED
1 GEDC
2 VERS 5.5.1
2 FORM LINEAGE-LINKED

Von den verschiedenen Werte interessieren uns SOUR, DESTVERS, NAME, CHAR, GEDC. Von wirklichem Nutzen sind in diesem Beispiel aber nur die verwendete Zeichensatzkodierung UTF-8 und die GEDCOM Version 5.5.1. GEDCOM unterstützt AMSEL, ASCII, UTF-8 und weitere UNICODE Varianten. Da ASCII für Europäer ungeeignet und AMSEL vollkommen exotisch ist, belassen wir es vorerst beim gängigen UTF-8. Da wir keine anderen Werte unterstützen, werfen wir an dieser Stelle eine ParseException.

private static void readEncoding(TokenReader reader) throws ParseException {
  String content = reader.getContent()
  try {
    GedcomEncoding encoding = GedcomEncoding.byName(content);
    if (encoding != GedcomEncoding.UTF_8) {
      throw new ParseException("Unsupported encoding: " + encoding);
    }
  } catch (RuntimeException e) {
    throw new ParseException("Illegal encoding: " + content);
  }
}

Datumsangaben für Ereignisse können ungenau  sein, manchmal ist der Tag nicht bekannt, manchmal ist auch nur das Jahr überliefert.

PERS: 
  REF: P1
DEATH: 
  DATE: 1769
BIRTH: 
  DATE: Aug 1742

Erfreulicherweise bietet uns das java.time Package alle notwendigen Klassen um mit unvollständigen Datumangaben umzugehen.

LocalDate, YearMonth und Year als Subklassen von Temporal entsprechen den Vorgaben des GEDCOM Standards. Wir machen es uns an dieser Stelle einfach und probieren einfach die erlaubten Formate nacheinander durch. Konnten wir die Datumsangabe konvertieren, dann speichern wie sie als Temporal im Ereignis.

private static Temporal parseDate(String temporal) {
  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[[dd ]MMM ]yyyy").withLocale(Locale.ENGLISH);
  try {
    TemporalAccessor accessor = formatter.parse(value);
    if (accessor.isSupported(ChronoField.DAY_OF_MONTH)) {
      return LocalDate.from(accessor);
    }
    if (accessor.isSupported(ChronoField.MONTH_OF_YEAR)) {
      return YearMonth.from(accessor);
    }
    return Year.from(accessor);
  } catch (DateTimeException e) {
    return null;
  }
}

Zum Ausgeben unserer Temporal Variablen genügt wiederum ein einfacher DateTimeFormatter Aufruf.

YearMonth deathDate = YearMonth.of(1796, Month.JANUARY);
DateTimeFormatter.ofPattern("[[dd ]MMM ]yyyy").format(deathDate);

Mit den hier vorgestellten Besonderheiten können viele Informationen aus einer GEDCOM Datei extrahiert werden, um im nächsten Schritt einen Stammbaum zu zeichnen.