Porf Praxis

Einleitung

Anfang letzten Jahres stand ich vor der Aufgabe, eine Liste mit Hashes als Tabelle in verschiedenen Formaten auszugeben. Dazu musste es doch Perl-Module auf dem CPAN geben. Gibt es auch, mehr als 5000 Treffer für "Report". Aber diese Reports sind spezialisiert für bestimmte Aufgaben und stellen meistens nur ein Ausgabeformat zur Verfügung.

Für "Report & Framework" gibt es genau einen Treffer: Data::Report, letzte Änderung am 17.08.2008, Version 0.10. Eine kurzer Blick in die Doku des Frameworks zeigt, dass es Klassen für die Plugins verwendet, die die Formate definieren. Der dort gewählte Ansatz kann zu sehr langsamer SW führen, Informationen über die Performance gibt es nicht.

Außerdem war mir die Anwendung der Frameworks/Reports, die ich mir angesehen habe, zu kompliziert und nicht Perl-like.

Deshalb startete ich mit der Entwicklung eines eigenen, allgemeinen, offenen Report-Frameworks: Perl Open Report Framework, kurz PORF, ist der Arbeitsname. (ORF ist schon durch irgendeinen östereichischen Fernsehsender belegt.)

Aktuell liegt die Version 0.950 für euch zum Download auf meiner Homepage bereit.

Falls jemand ein Report-Framework mit ähnlichen Eigenschaften kennt, informiert mich bitte. Dann kann ich mir die Weiterentwicklung von PORF eventuell sparen.

Einen Report in 4+n Statements konfigurieren und ausgeben

Einen Report mit 4+n Statements zu konfigurieren und die Ergebnisse in eine Datei zu schreiben, ist das Ziel von PORF und mit der aktuellen Version bereits performant umgesetzt:

  use Report::Porf::Framework;  # "use" zaehle ich nicht als Statement

  # --- Report erzeugen lassen ---

  my $report_framework = Report::Porf::Framework::Get();             # 1.
  my $report           = $report_framework->CreateReport($format);   # 2.

  # --- Spalten konfigurieren, die Daten liegen als Hash vor ---

  # --- n = 3 für 3 Spalten ---
  $report->ConfigureColumn(-header => 'Vorname',  -value_named => 'Prename' ); # lang
  $report->ConfCol        (-h      => 'Nachname', -val_nam     => 'Surname' ); # kurz
  $report->CC             (-h      => 'Alter',    -vn          => 'Age'     ); # minimal

  # --- Konfiguration abschließen, das ist notwendig ---

  $report->ConfigureComplete();                                      # 3.

  # --- Daten ausgeben ---

  $report->WriteAll($person_rows, $out_file_name);                   # 4.

  # --- Fertig ! ---

Erste Tests mit Kollegen ergaben, dass sie die API von PORF genauso leicht verständlich finden wie ich. Im einzelnen:

0. Use

Es existieren bislang keine weiteren Abhängigkeiten zu anderen Perl-Modulen, bis auf einige absolute Basis-Module wie z.B. FileHandle und Exporter. Auch das Report-Modul selbst benötigt kein use, weil kein Report->new() durch den Anwender aufgerufen werden muss (darf).

1. Framework erzeugen

Mit Anweisung 1 holt man sich ein vorkonfiguriertes Report-Framework ab. Es ist sehr wichtig, sich nicht selbst eines mit "new" zu erzeugen, denn die Konfiguration des Frameworks und der Reports erfordert einige Arbeit in der "Framework-Factory".

2. Report erzeugen

Das Framework kann jetzt unverändert (Out Of The Box) verwendet werden, um einen Report im Format $format zu erstellen. Als Formate sind aktuell "Text", "HTML" und "CSV" untersützt, weitere sind geplant. Vielleicht schaffen wir es ja im Rahmen des Perl-Workshops, die Report-Konfiguratoren für Wikis und LaTeX zu erstellen.

Zur Konfiguration des Reports werden die Klassen HtmlReportConfigurator, TextReportConfigurator und CsvReportConfigurator aus Report::Porf::Table::Simple verwendet, die den Report gesteuert durch das Framework unsichtbar im Hintergrund vorkonfigurieren. Man kann beliebige eigene Report-Konfiguratoren - auch für neuen Formate - erstellen und in einer eigenen Frameworkinstanz bereitstellen, ohne das Framework selbst irgendwie ändern zu müssen.

CreateReport($format) liefert eine Instanz der Klasse Report::Porf::Table::Simple zurück, die einen einfachen tabellarischen Report im geforderten Ausgabeformat erzeugen kann. Es handelt sich hier immer um dieselbe Klasse, unabhängig vom gewünschten Format. Das vereinfacht die Anwendung sehr, denn so fängt man sich keine (unerwünschten) Abhängigkeiten zu weiteren Klassen/Modulen ein.

1..n Spalten des Reports konfigurieren

Je nach Geschmack und Erfahrung kann man zwischen kurzen und langen Bezeichnern wählen. Hier ist die minimale Konfiguration angegeben: Überschrift und Zugriff auf den Wert einer Zelle (Cell). Ein Datensatz für eine Datenzeile (Row) liegt hier als Hash vor. Arrays, Klassen und freie Zugriffe werden auch unterstützt.

3. Konfiguration abschließen

Der Abschluss der Konfiguration mit $report->ConfigureComplete(); ist notwendig, da es viele Möglichkeiten gibt, die anschließende Ausgabe durchzuführen. Außerdem kann man die Konfiguration ab jetzt nicht mehr verändern. Der Versuch, noch einmal $report->ConfigureColumn(...); aufzurufen, endet in einer Warnung.

4. Daten in Datei schreiben

Wählt man das Text-Format und schreibt man die Daten in eine Datei, erhält man folgendes Ergebnis:

  *============+============+============*  # Fette Trennlinie
  |   Vorname  |  Nachname  |    Alter   |  # Überschriften
  *------------+------------+------------*  # Normale Trennlinie
  | Vorname 1  | <Zelle>    | 7.69230769 |  # Eine Datenzeile mit Zellen
  | Vorname 2  | Name 2     | 15.3846153 |
  | Vorname 3  | Name 3     | 23.0769230 |
  | Vorname 4  | Name 4     | 30.7692307 |
  *============+============+============*

Man kann die Datenausgabe auch zeilenweise oder sogar zellweise durchführen lassen oder sich nur die Ergebnisse als String abholen, andernfalls hätte das Framework den Titel "Offen" nicht verdient.

Eine Zeile als String liefert:

  my $line = $report->GetRowOutput($data_ref);

Aber Achtung, falls man Trennzeilen definiert hat, kann $line auch mehrere Zeilen getrennt mit '\n' enthalten.

Weitere Konfigurationsmöglichkeiten

Wechsel des Ausgabeformats

Um die Tabelle als HTML auszugeben, belegt man $format einfach mit "HTML" statt "Text", und führt den Code nocheinmal durch.

Es bietet sich an, alle Zeilen bis auf Anweisung 4., Daten schreiben, in eine eigene sub zu packen, z.B.

  sub CreateAgeReport {
      my $format = shift;

      my $report_framework = Report::Porf::Framework::Get();             # 1.
      my $report           = $report_framework->CreateReport($format);   # 2.

      $report->ConfigureColumn(-header => 'Vorname',  -value_named => 'Prename' ); # lang
      $report->ConfCol        (-h      => 'Nachname', -val_nam     => 'Surname' ); # kurz
      $report->CC             (-h      => 'Alter',    -vn          => 'Age'     ); # minimal

      $report->ConfigureComplete();                                      # 3.

      return $report;
  }

Dann kann man die Daten als HTML oder Text ausgeben mit

  my $report = CreateAgeReport($format);
  $report->WriteAll($person_rows, $out_file_name);

und einen zweiten Report mit

  $report->WriteAll($person_rows_2, $out_file_name_2);

Konfiguration der Spalten

Bis hierher hätte man das alles noch irgendwie mit join(...) erledigen können. Aber einige der folgenden Konfigurationsmöglichkeiten für die Spalten sind damit nicht mehr leicht implementierbar.

Layout-Optionen

  -header  -h   constant: Text
  -align   -a   constant: (left|center|right)
                          (l   |   c  |    r)
  -width   -w   constant: integer    # Der Text wird gekürzt/verlängert
  -format  -f   constant: string für sprintf
  -color   -c   constant / sub {...}

Die sub {...} ermöglicht eine bedingte Einfärbung durch die Daten auf eine einfache Art. Hier zeigt sich einmal mehr die "Magie des EVAL {}" (siehe 2. Vortrag). Wie das genau funktioniert, erläutert das zweite Beispiel.

Nicht alle Optionen sind in jedem Format verfügbar. Unbekannte Optionen werden einfach ignoriert.

Datenzugriff

Daten liegen in Perl typischerweise als Array, Hash oder Instanz einer Klasse vor. Oder auch irgendwie anders. Für jeden Datentyp gibt es eigene, komfortable Zugriffsmethoden durch den Report. Es ist sogar möglich, sie miteinander zu kombinieren (falls die Daten das hergeben...)

GetValue Alternative 1 --- ARRAY

  my $prename = 1;
  my $surname = 2;
  my $age     = 3;

  $report->ConfigureColumn(-header => 'Vorname',  -value_indexed => $prename ); # long
  $report->ConfCol        (-h      => 'Nachname', -val_idx       => $surname ); # short
  $report->CC             (-h      => 'Alter',    -vi            => $age     ); # minimal

GetValue Alternative 2 --- HASH

  $report->ConfigureColumn(-header => 'Vorname',  -value_named => 'Prename' ); # long
  $report->ConfCol        (-h      => 'Nachname', -val_nam     => 'Surname' ); # short
  $report->CC             (-h      => 'Alter',    -vn          => 'Age'     ); # minimal

GetValue Alternative 3 --- OBJECT

  $report->ConfigureColumn(-header => 'Vorname',  -value_object => 'GetPrename()'); # long
  $report->ConfCol        (-h      => 'Nachname', -val_obj      => 'GetSurname()'); # short
  $report->CC             (-h      => 'Alter',    -vo           => 'GetAge()'    ); # minimal

GetValue Alternative 4 --- Free

  $report->ConfigureColumn(-h => 'Vorname',    -value =>    '"Dr. " . $_[0]->{Prename}'    );
  $report->ConfCol        (-h => 'Nachname',     -val => sub { return $_[0]->{Surname}; }; );
  $report->CC             (-h => 'Alter (Monate)', -v =>     '(12.0 * $_[0]->GetAge())'    );

Der Report erzeugt sich in jedem Fall eine anonyme sub, ähnlich wie bei 'Nachname' und 'Alter' in Alternative 4. In der aktuellen Implementierung ist das ein mehrstufiger Prozess. Man sollte sehr darauf achten, hier keinen Syntaxfehler in den "value"-Optionen einzubauen. Die Fehlersuche kann sich sehr schwierig gestalten. Das ist ein Nachteil des Frameworks, der sich nur vermeiden lässt, wenn man die Performance und den Komfort drastisch reduziert.

Zur leichteren Problemanalyse kann man sich Trace-Informationen ausgeben lassen mit:

  $report->SetVerbose(3)

liefert für die Spalte 'Alter' z.B.

  #------------------------------
  -h = Alter
  -vn = Age
  ### eval_str = sub { return $_[0]->{Age}; }
  ### ref(sub_ref) CODE
  width 10
  ### eval_str = sub { return  ConstLengthLeft(10, $value_action->($_[0])) |; }
  ### ref(cell_action) CODE
  ### AddCellOutputAction: CODE(0x285251c)

Weitere Beispiele

Bedingte Einfärbung

Für alle Personen aus der Liste, die noch nicht mindestens 18 Jahre alt sind, soll die Alter-Zelle rot eingefärbt werden. Dazu benötigt man eine sub {} für die Konfiguration der Zellfarbe:

  $report->ConfigureColumn(
     -header  => 'Age',
     -width   => 15,
     -align   => 'Right',
     -format  =>  "%.3f years",
     -color   =>  sub { return $_[0]->{Age} >= 18 ? "": '#EECCCC'; },
     -vn      => 'Age', );

Außerdem wird mit "-format" noch die Anzahl der Nachkommastellen auf 3 beschränkt. Man beachte, dass man in der Sub Zugriff auf die/den gesamten Datensatz/-zeile hat, sodass auch Bedingungen mit mehreren Parametern als Input möglich sind.

Spezialanzeige für Werte

In diesem Beispiel wird die Augenanzahl eines Würfels im HTML-Report auch in einer zusätzlichen Spalte als Grafik angezeigt:

# =image dice_table.png Tabelle mit Würfeln

Dazu benötigt man zunächst 10 Bilder, die 0 bis 9 Augen auf einer Würfelseite anzeigen. Je nach Wert des Wurfs wird dann das entsprechende Bild in der HTML-Tabelle angezeigt.

Dazu wird eine spezielle -value Option verwendet: sub { return $dices_to_image->($_[0]->{'Dices'}); }.

Insgesamt erhält man:

  $report->ConfigureColumn
      (-header => 'Dices', -a => 'C', 
       -value => sub {
           return $dices_to_image->($_[0]->{'Dices'});
        }
      ) if $report->IsFormat('HTML');

Für alle, die noch nicht so häufig mit dem sub {} Befehl gearbeitet haben: $dices_to_image ist eine Referenz auf die aufzurufende Funktion, und die Variable wird automatisch vom erzeugenden Code in die anonyme sub{} importiert, inklusive aller anderen bekannten Variablen und Funktionen. Eine solche 'Sub' wird dann als 'Closure' bezeichnet.

$_[0] ist die erste Variable aus der Argumentenliste, die für die -value option immer mit dem auszugebenden Datensatz belegt ist, hier also mit der Wurfnummer und den erzielt(en) Wert(en) des Würfel-Wurfs: $_[0]->{'Dices'} enthält eine Abfolge gewürfelter Werte als String.

Das nachgestellte if sorgt dafür, dass diese zusätzliche Spalte nur erzeugt wird, falls es sich um einen HTML-Report handelt.

$dices_to_image wird folgendermaßen definiert:

  my $dices_to_image = sub {
    my $throws = shift; # Dice throw values

    my $result = '';
    my $number_html_code;

    foreach my $number (split (//, $throws)) {
      if ($number =~ /^\d$/) {
        $number_html_code = "<img src='dice_$number.jpg'/>"
      }
      else {
        $number_html_code = "?";
      }
      $result .= $number_html_code;
    }

    return $result;
  };

Bis auf die erste Zeile eine Übungsaufgabe für den Perl-Fortgeschrittenen-Kurs. Also eine kleine Fingerübung für erfahrene Perl-Programmierer. Zurück liefert diese Sub eine Abfolge von <img ...> HTML-Befehlen, die im Browser die Bilder mit den Würfelaugen in der gewünschten Reihenfolge ausgibt.

Ein direkter Aufruf einer "normalen" Sub wäre auch möglich, aber durch die Verwendung einer anonymen Sub geht man allen Problemen bei der Namensauflösung aus dem Weg, die andernfalls entstehen könnten.

Gleichzeitig Verwendung mehrerer Frameworks

Im etwas größeren Beispiel "Fussball" im gleichnamigen Ordner werden mehrere Frameworks gleichzeitig erzeugt und durch die Aufrufoptionen wird - eventuell - eines ausgewählt. Dadurch ist es möglich, das Layout (z.B. Farben) sowie begleitende Texte zu verändern, ohne bei der Programmierung der Listen auch nur an Varianten zu denken.

Beispiele starten

Wie man die Beispiele startet, steht in demo.txt, das wie die Beispiele im Ordner Practice abgelegt ist.

Auf die Erstellung von Start-Scripts habe ich bewusst verzichtet, denn bis auf ein zusätzliches

  -I../lib

wird einfach nur Perl aufgerufen.

Vortrag

Ich werde verschiedene kleine und große Beispiele zeigen und die verschiedenen Anwendungsmöglichkeiten von PORF erläutern, vom Einsatz in kleinen Scripts bis zu großen Applikationen.

Eigenschaften

Im folgenden möchte ich noch die wesentlichen Eigenschaften erläutern, die ich so bei anderen Report-Modulen nicht gefunden habe:

Offen

Es ist leicht möglich, eigene Varianten von Report-Konfiguratoren zu erstellen oder neue Formate zu unterstützen. Auch die gleichzeitige Verwendung von verschiedenen Konfiguratoren für dasselbe Format ist durch die Möglichkeit, mehrere Framework-Instanzen zu verwenden, sehr einfach zu realisieren.

Unabhängigkeit von anderen Modulen

Bewusst verwendet (Standard)-PORF z.B. keine HTML-Funktionen aus CPAN-Modulen, damit das System wirklich offen bleibt. Der Anwender kann selbst entscheiden, welche Module er in eigenen/adaptierten Report-Konfiguratoren verwendet.

Performance

Auf meinem alten Win-XP Laptop mit 1 GByte Hauptspeicher, 800 MHz AMD 64 Bit Single-Core und Perl 5.14.2 konnte man zwischen 50.000 und 100.000 Zellen (nicht Zeilen!) pro Sekunde in eine Datei auf die Festplatte schreiben.

Auf aktuellen Systemen können mehr als 200.000 Zellen pro Sekunde erzeugt werden, für 1 Mio Zellen werden damit weniger als 5 Sekunden benötigt.

Zweck und Eingrenzung der Funktionalität

PORF wurde entwickelt, um Massendaten leicht, komfortabel, flexibel und schnell ausgeben zu können, bevorzugt in Listenform. Die Konfiguration soll generisch und Perl-Like funktionieren.

Datenbeschaffung und -Verarbeitung sind nicht Bestandteil des Frameworks und sollen es auch nicht sein. PORF kümmert sich nur um Formatierung und Ausgabe, ist also ein komfortables "Printing" Modul.

Diagramme und längerer Text können von anderen Tools besser bereitgestellt werden und sind daher auch kein Bestandteil von PORF. Aber da man den vollen Durchgriff auf die Reports hat, kann man Teile (Tabellen) erstellen, die man in anderen Dokumenten verwenden/importieren kann.

Wie Porf intern aufgebaut ist und warum, wird in meinem 2. Vortrag

  "The Magic Of Eval"

vorgestellt.

Ausblick

Zur Zeit werden nur einfache Listen unterstützt. Mehrzeilige Ausgaben, mehrzeiliger Aufbau mit Verbundzellen, Visitenkarten-Layouts bis hin zur freien Platzierung von Werten auf der Seite sind angedacht.

Aber in der Erstellung einer einfachen, konsistenten API steckt noch viel Arbeit. Wer in irgeneiner Weise mitarbeiten möchte, ist dazu herzlich eingeladen.

Weitere Konfiguratoren für Wikis und LaTeX sind in Vorbereitung.

Autor und Kontakt

  Ralf Peine
  Jahrgang 1965 

  Dipl. Mathematik 1991 
  Software-/Tool-/CM-Architekt 

  Renesas Electronics Europe GmbH

Ich programmiere seit ca. 20 Jahren mit wachsendem Vergnügen mit Perl, beherrsche aber auch viele andere Sprachen wie C/C++/C#, VB, PHP, Lisp, Java-Script, HTML, XML... Von Großrechner-SW bis zur Microcontroller-Digitaltechnik (wo ich mich jetzt bewege) habe ich schon so manches entworfen und entwickelt.

Meine private Web-Domain, dort könnt ihr euch PORF herunterladen:

  * http://www.jupiter-programs.de/
  * http://www.jupiter-programs.de/prj_public/porf/index.htm

Anmerkungen zu PORF könnt ihr in meinen Blog schreiben unter:

http://blogs.perl.org/users/jpr65/2013/05/perl-open-report-framework-0901-released.html

Meine aktuellen Favoriten in der SW-Entwicklungsmethodik sind

  * Testdriven Development (nie mehr anders!)
  * Operation und Integration
    (http://blog.ralfw.de/2013/04/software-fraktal-funktionale.html)

"So einfach wie möglich, aber nicht einfacher" (Albert Einstein)

Have Fun Using PORF

Ich würde mich freuen, von euren Erfahrungen mit PORF zu hören und wünsche euch viel Spaß mit Perl :))

Ralf.