Leitweg ID – Ihre Nummer für die Behörde

Als Software Entwickler lernt man immer wieder neue Varianten eindeutiger Kennzeichner kennen. Da gibt es die bekannten Kennzeichner wie die Telefonnummern, Kreditkartennummern, PLZ, ISBN, EAN-13, IATA, IBAN, BIC, die veraltete BLZ und nicht so bekannte wie die internationalen Patentnummern, Kreisgemeindeschlüssel und die Leitweg ID. Alle diese Kennzeichner müssen erkannt, geprüft, interpretiert und geschrieben werden können.

Laut Wikipedia ist die Leitweg ID “ein Kennzeichen einer elektronischen Rechnung zur eindeutigen Adressierung von öffentlichen Auftraggebern in Deutschland (Beispiele: Behörden, Kommunen, Ministerien)”.

Technisch gesehen ist sie eine Verkettung aus einer Grobadressierung, einer Feinadressierung und einer Prüfsumme. Die Grobadressierung besteht entweder aus dem Kennzeichen für den Bund und einer Ordnungskennzahl des Bundes oder aus dem Kennzeichen für Bundesland, Regierungsbezirk, Landkreis und der Gemeinde. Die Grobadressierung besteht aus mindestens zwei Ziffern und kann maximal 12 Ziffern lang sein.

BestandteilFormat
Kennzeichen von Bundesland oder Bundzwei Ziffern
Kennzeichen Regierungsbezirk oder Ordnungskennzahl des Bundeseine Ziffer
Kennzeichen Landkreiszwei Ziffern
Kennzeichen Gemeindedrei, vier oder sieben Ziffern
Bestandteile der Grobadressierung

Verpflichtend ist hier nur das Kennzeichen von Bundesland oder Bund, die anderen Bestandteile sind dahingehend optional, dass der Regierungsbezirk angegeben sein muss, wenn ein Landkreis angegeben ist und ein Landkreis angegeben sein muss, wenn eine Gemeinde angegeben ist. Bei einer Grobadressierung für den Bund entfallen Landkreis und Gemeinde Kennzeichen.

Die optionale Feinadressierung ist von der Grobadressierung mit dem entsprechend optionalen Trennzeichen -separiert und kann bis zu 30 Buchstaben und Ziffern enthalten. Bislang sind mir noch keine Feinadressierungen mit Buchstaben untergekommen, in NRW gibt es beispielsweise keine einzige Leitweg ID die Buchstaben enthält. Die Buchstaben verkomplizieren die Implementierung des Prüfsummenverfahrens und bergen die üblichen Gefahren von Verwechslungen. Bei dreißig Stellen in der Feinadressierung und damit einer Quintillion Möglichkeiten, sollte auch der deutsche Bürokratiewahn ausreichend versorgt sein.

Die Prüfsumme ist mit dem Trennzeichen - am Ende der Leitweg ID angefügt und berechnet sich aus der Differenz der Zahl 98 zu der Zahl, die sich aus der Verkettung von Grobadressierung und Feinadressierung und leerer Prüfsumme modulo 97 berechnet:

prüfsumme = 98 - (grobadressierung \circ feinadressierung \circ "00") \mod 97 

Die Prüfsumme ist korrekt wenn gilt:

prüfsumme \textrm{ ist korrekt} \Leftrightarrow  (grobadressierung \circ feinadressierung \circ prüfsumme) \mod 97 = 1

In der folgenden Tabelle sind mehrere Beispiele für eine korrekte Leitweg ID dargestellt. Bei der Deutsche Bahn AG besteht die Grobadressierung aus der 99, dem Kennzeichen für dem Bund und der 2 als Ordnungskennzahl für Empfänger von elektronische Rechnungen über die OZG-RE. Bei der Stadt Xanten, der Universität Bielefeld und der Aachener Parkhaus GmbH beginnt die Grobadressierung mit den Ziffern 05 für das Bundesland NRW. Danach folgt eine Ziffer für den jeweiligen Regierungsbezirk und danach zwei Ziffern für den Landkreis. Während bei der Aachener Parkhaus GmbH und der Stadt Xanten jeweils noch ein siebenstelliger Gemeindeschlüssel folgt, fehlt dieser bei der Universität Bielefeld, weil Bielefeld eine kreisfreie Stadt ist.

Leitweg IDName der Behörde
053340002002-33004-23 Aachener Parkhaus GmbH
992-90009-96Deutsche Bahn AG
05711-06001-79Universität Bielefeld
051700052052-31001-35Stadt Xanten
Beispiele für Leitweg IDs

Die Leitweg ID soll durch immutable Klasse realisiert werden, die aus Textdarstellung oder den einzelnen Komponenten erstellt werden kann. Außerdem soll die toString Methode die korrekte Darstellung liefern.

public final class LeitwegId {
  private final GrobAdressierung grobAdressierung;
  private final String feinAdressierung;
 
  public LeitwegId(GrobAdressierung grobAdressierung, String feinAdressierung) {
    this.grobAdressierung = requireNonNull(grobAdressierung);
    this.feinAdressierung = requireNonNull(feinAdressierung);
  }

  public GrobAdressierung getGrobAdressierung() {
    return grobAdressierung;
  }

  public Optional<String> getFeinAdressierung() {
    return feinAdressierung.isEmpty() ? Optional.empty() : Optional.of(feinAdressierung);
  }
}

Die beiden Komponenten der LeitwegId werden im Konstruktor übergeben und in den finalen Feldern grobAdressierung und feinAdressierung gespeichert. Die Get-Methode für die feinAdressierung liefert ein Optional zurück, weil die Feinadressierung ein optionaler Bestandteil der Leitweg ID ist.

Die GrobAdressierung ist ein Interface, dass von den Klassen LandesAdressierung und BundesAdressierung implementiert wird. Es besitzt zwei Default-Methoden, die von der entsprechenden Implementierung überschrieben wird. Mit diesen Methoden kann ohne einen Cast auf die korrekte Implementierung zugegriffen werden.

public final class LeitwegId {
  public interface GrobAdressierung {
    default Optional<LandesAdressierung> asLandesAdressierung() {
      return Optional.empty();
    }

    default Optional<BundesAdressierung> asBundesAdressierung() {
      return Optional.empty();
    }
  }

  public final static class LandesAdressierung implements GrobAdressierung {
    private final GermanFederalState bundesland;
    private final String regierungsbezirk;
    private final String landkreis;
    private final String gemeinde;

    public LandesAdressierung(GermanFederalState bundesland, String regierungsbezirk, String landkreis, String gemeinde) {
      this.bundesland = requireNonNull(bundesland);
      this.regierungsbezirk = requireNonNull(regierungsbezirk);
      this.landkreis = requireNonNull(landkreis);
      this.gemeinde = requireNonNull(gemeinde);
    }

    @Override
    public String toString() {
      return bundesland.getId() + regierungsbezirk + landkreis + gemeinde;
    }

    @Override
    public Optional<LandesAdressierung> asLandesAdressierung() {
      return Optional.of(this);
    }

    public GermanFederalState getBundesland() {
      return bundesland;
    }

    public Optional<String> getRegierungsbezirk() {
      return regierungsbezirk.isEmpty() ? Optional.empty() : Optional.of(regierungsbezirk);
    }

    public Optional<String> getLandkreis() {
      return landkreis.isEmpty() ? Optional.empty() : Optional.of(landkreis);
    }

    public Optional<String> getGemeinde() {
      return gemeinde.isEmpty() ? Optional.empty() : Optional.of(gemeinde);
    }
  }

  public final static class BundesAdressierung implements GrobAdressierung {
    private final RechnungseingangsPlattform plattform;

    public BundesAdressierung(RechnungseingangsPlattform plattform) {
      this.plattform = requireNonNull(plattform);
    }

    @Override
    public String toString() {
      return "99" + plattform.getValue();
    }

    @Override
    public Optional<BundesAdressierung> asBundesAdressierung() {
      return Optional.of(this);
    }

    public RechnungseingangsPlattform getPlattform() {
      return plattform;
    }
  }
}

Die LandesAdressierung beinhaltet die Komponenten der Grobadressierung in den Bundesländern. Hier wird die Enum GermanFederalState aus der Holiday Implementierung verwendet. Alle Komponenten die optional in der Grobadressierung sein können, werden als Optional zurückgegeben.

Um die API etwas einfacher zu gestalten, werden Convenience Methoden und nicht die Konstruktoren verwendet.

public final class LeitwegId {
  public static LeitwegId from(GermanFederalState bundesland, String regierungsbezirk, String landkreis, String gemeinde, String feinAdressierung) {
    return new LeitwegId(new LandesAdressierung(bundesland, regierungsbezirk, landkreis, gemeinde), feinAdressierung);
  }

  public static LeitwegId from(GermanFederalState bundesland, String regierungsbezirk, String landkreis, String gemeinde) {
    return new LeitwegId(new LandesAdressierung(bundesland, regierungsbezirk, landkreis, gemeinde), "");
  }

  public static LeitwegId from(GermanFederalState bundesland, String regierungsbezirk, String landkreis) {
    return new LeitwegId(new LandesAdressierung(bundesland, regierungsbezirk, landkreis, ""), "");
  }

  public static LeitwegId from(GermanFederalState bundesland, String regierungsbezirk) {
    return new LeitwegId(new LandesAdressierung(bundesland, regierungsbezirk, "", ""), "");
  }

  public static LeitwegId from(GermanFederalState bundesland) {
    return new LeitwegId(new LandesAdressierung(bundesland, "", "", ""), "");
  }

  public static LeitwegId from(RechnungseingangsPlattform plattform, String feinAdresssierung) {
    return new LeitwegId(new BundesAdressierung(plattform), feinAdresssierung);
  }
}

Da alle Konstruktoren nur in diesen Methoden verwendet werden, können sie private deklariert werden.

Zur Ausgabe der Leitweg ID fehlt noch die berechnete Prüfsumme. Diese wird berechnet, in dem eine BigInteger Instanz aus grobAdressierung, feinAdressierung und "00" erzeugt wird. Da die String Darstellung von grobAdressierung und "00" nur Ziffern enthalten können sie einfach konkateniert werden. Für die Buchstaben in der feinadressierung werden diese auf entsprechende Darstellungen im Bereich von "10" bis "36" abgebildet.

public final class LeitwegId {
  private static final BigInteger VALUE_97 = BigInteger.valueOf(97);
  private static final BigInteger VALUE_98 = BigInteger.valueOf(98);

  private String calculateChecksum() {
    BigInteger number = generateNumber(grobAdressierung, feinAdressierung, "00");
    String result = VALUE_98.subtract(number.mod(VALUE_97)).toString();
    return result.length() == 1 ? "0" + result : result;
  }

  private static BigInteger generateNumber(GrobAdressierung grobAdressierung, String feinAdressierung, String checksum) {
    StringBuilder builder = new StringBuilder();
    builder.append(grobAdressierung);
    for (char c : feinAdressierung.toCharArray()) {
      if (c >= '0' && c <= '9') {
        builder.append(c);
      } else if (c >= 'A' && c <= 'Z') {
        builder.append(c - 'A' + 10);
      } else {
        builder.append(c - 'a' + 10);
      }
    }
    builder.append(checksum);
    return new BigInteger(builder.toString());
  }
}

Da die LeitwerkId Instanz sich nicht verändern kann, ist die Neuberechnung der Prüfsumme und der String Repräsentation unnötig. Daher verwendet die Klasse zwei Felder die bei der ersten Verwendung der Werte gefüllt werden.

public final class LeitwegId {
  private String checksum;
  private String value;

  public String getChecksum() {
    checksum = requireNonNullElseGet(checksum, this::calculateChecksum);
    return checksum;
  }

  private String asString() {
    StringJoiner joiner = new StringJoiner("");
    joiner.add(grobAdressierung.toString());
    if (!feinAdressierung.isEmpty()) {
      joiner.add("-").add(feinAdressierung);
    }
    return joiner.add("-").add(getChecksum()).toString();
  }

  @Override
  public String toString() {
    value = requireNonNullElseGet(value, this::asString);
    return value;
  }
}

Für die Implementierung von equals und hashCode müssen diese beiden berechneten Werte nicht berücksichtigt werden. Die LeitwegId Instanzen können also verwendet werden ohne eine einzige Prüfsummenberechnung.

Es fehlt noch die Erzeugung einer LeitwegId Instanz aus einer String Darstellung. Dies übernimmt die parse Methode. Sie prüft den übergebenen String anhand eines regulären Ausdrucks und erzeugt dann basierend auf den gefundenen Feldern eine LeitwegId mit BundesAdressierung oder LandesAdressierung.

public final class LeitwegId {
  private static final BigInteger VALUE_01 = BigInteger.valueOf(1);
  private static final BigInteger VALUE_97 = BigInteger.valueOf(97);
  private static final BigInteger VALUE_98 = BigInteger.valueOf(98);

  private static final Pattern pattern = Pattern
      .compile("^(\\d{2})((\\d)((\\d{2})(\\d{3}|\\d{4}|\\d{7})?)?)?(-([A-Za-z0-9]{1,30}))?-(\\d{2})$");

  public static LeitwegId parse(String value) {
    Matcher matcher = pattern.matcher(value);
    if (!matcher.matches()) {
      throw new IllegalArgumentException("cannot parse id: " + value);
    }

    String countryOrState = matcher.group(1);
    GrobAdressierung grobAdressierung;
    if ("99".equals(countryOrState)) {
      if (matcher.group(5) != null || matcher.group(6) != null) {
        throw new IllegalArgumentException("cannot parse id: " + value);
      }
      grobAdressierung = new BundesAdressierung(RechnungseingangsPlattform.byId(matcher.group(3)).orElseThrow());
    } else {
      GermanFederalState bundesland = GermanFederalState.byId(countryOrState).orElseThrow();
      String regierungsbezirk = requireNonNullElse(matcher.group(3), "");
      String landkreis = requireNonNullElse(matcher.group(5), "");
      String gemeinde = requireNonNullElse(matcher.group(6), "");
      grobAdressierung = new LandesAdressierung(bundesland, regierungsbezirk, landkreis, gemeinde);
    }
    String feinAdressierung = requireNonNullElse(matcher.group(8), "");
    String checksum = matcher.group(9);
    return complete(proof(value, grobAdressierung, feinAdressierung, checksum), value, checksum);
  }

  private static LeitwegId complete(LeitwegId leitwegId, String value, String checksum) {
    leitwegId.checksum = checksum;
    leitwegId.value = value;
    return leitwegId;
  }

  private static LeitwegId proof(String value, GrobAdressierung grobAdressierung, String feinAdressierung,
      String checksum) {
    BigInteger number = generateNumber(grobAdressierung, feinAdressierung, checksum);
    if (!number.mod(VALUE_97).equals(VALUE_01)) {
      throw new IllegalArgumentException("invalid checksum: " + value);
    }
    return from(grobAdressierung, feinAdressierung);
  }
}

Bevor die LeitwegId Instanz aus GrobAdressierung und FeinAdressierung zusammengebaut wird, muss die gefundene Prüfsumme in der Methode proof validiert werden. So können keine fehlerhaften LeitwegId Instanzen eingelesen werden. Eine kleine Optimierung ist die Methode complete. Da beim Einlesen der LeitwegId Instanz die Prüfsumme und die String Darstellung bekannt sind, werden sie in der LeitwegId Instanz gespeichert. Die so erzeugten LeitwegId Instanzen müssen ihre Prüfsumme und String Darstellung nicht erneut berechnen.

Damit ist die Implementierung einer Klasse zur Behandlung von Leitweg IDs auch schon abgeschlossen und kann wie im folgenden Beispiel verwendet werden.

LeitwegId parsedLeitwegId= LeitwegId.parse("05711-06001-79");
LandesAdressierung landesAdressierung = parsedLeitwegId.getGrobAdressierung().asLandesAdressierung().orElseThrow();
GermanFederalState state = landesAdressierung.getBundesland();

LeitwegId generatedLeitwegId = LeitwegId.from(GermanFederalState.NW, "3", "34", "0002002", "33004");
String checksum = generatedLeitwegId.getChecksum();