Schlagwort: Bugs

Regex Mysterium

06. November 2011

Regex Mysterium
In any project, there will be a certain amount of unknown. This is the nature of our work. schreibt Ka Wai Cheung in the developer’s code. Bei der Frontend-Entwicklung beziehe ich diesen Satz zumeist auf die "große Unbekannte" – den Client (von der Hard- und Software bis hin zum Interaktionsverhalten), aber auch die Sprache JavaScript und ihre Implementationen bieten immer wieder echte Überraschungen.

Eine triviale Aufgabe: ein Datum validieren und mit einem anderen Datum vergleichen, dabei kann das Datum eine gewisse Varianz in der Reihenfolge und Schreibweise haben. Ich betrachtete kurz die bereits implementierte Bestandslösung für die Validierung und war erstaunt über das mühsame Arbeiten mit substr. Das kann man doch eleganter lösen: Ein regulärer Ausdruck für die Validierung, dient gleichzeitig der Zerlegung des Datums, um daraus dann ein Date Objekt für den Vergleich zu erstellen:

// eine Pruefung unter vielen, Abbruch der gesamten Pruefung wenn fehlerhaft...
proceed = false;
var date_test = [ '07-11-2011' , '05-11-2011' ];
var date_regexp = /^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/;
 
if( date_test[0].match( date_regexp ) &&
    date_test[1].match( date_regexp ) ) {
 
   temp = date_test[0].split( date_regexp );
   date_A = new Date();
   date_A.setFullYear( temp[3],  ( temp[2] -1 ) , temp[1] );
 
   temp = date_test[1].split( date_regexp );
   date_B = new Date();
   date_B.setFullYear( temp[3],  ( temp[2] -1 ) , temp[1] );
 
 // Datum A muss vor Datum B liegen
  if( date_A < date_B ) {
     proceed  = true;
  }
}
// Ende der Pruefung - nun Entscheidung...
alert( proceed ); // Test Ausgabe
...

Auf den ersten Blick schien alles zu funktionieren. Wie erwartet wird "false" ausgegeben - Task erledigt, Ticket schließen, Feierabend machen. Nächster Morgen - Ticket ist wieder auf: Die Erweiterung funktioniert nicht. Alle folgenden Prüfungen werden nicht mehr ausgeführt, unabhängig vom Datum. Kann nicht sein, ich hatte das Datum definitiv mehrfach verändert, um alle Variationen zu kontrollieren. Teste daraufhin die Anwendung im Firefox erneut - alles wie gewünscht, Screenshots an das Ticket gehängt, Rückmeldung. Geht Nicht! Noch ein Test, diesmal im Internet Explorer, und siehe da, im Internet Explorer bleibt proceed immer false.

Ein Fehler im regulären Ausdruck? Nicht wirklich, denn der match funktioniert cross-browser. Aber das Datum ist im Internet Explorer NaN während alle anderen getesteten Browser das korrekte Datum ausgeben. Scheitern tut das ganze am split. Laut Spezifikation kann die String Methode sowohl mit Zeichenketten, als auch regulären Ausdrücken umgehen. De facto gibt es da aber einige Unterschiede:

var test_string = 'a,b,c,,,d,e,f,g';
 
// Split mit Zeichenkette
// Erwartet: 9 -> Ergebnis ist 9
var test_a = test_string.split(',');
alert(test_a.length);
 
// Split mit regulärem Ausdruck:
// Erwartet: 9 -> Ergebnis ist im IE: 7
var test_b = test_string.split(/,/);
alert(test_b.length);

Die Methode split reagiert eindeutig anders als die Methode exec des RegExp Objektes. Ändert man das ursprüngliche Script wie folgt:

// eine Pruefung unter vielen, Abbruch der gesamten Pruefung wenn fehlerhaft...
proceed = false;
var date_test = [ '07-11-2011' , '05-11-2011' ];
var date_regexp = /^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/;
 
if( date_test[0].match( date_regexp ) &&
    date_test[1].match( date_regexp ) ) {
 
   temp = date_regexp.exec( date_test[0] );
   date_A = new Date();
   date_A.setFullYear( temp[3],  ( temp[2] -1 ) , temp[1] );
 
   temp = date_regexp.exec( date_test[1] );
   date_B = new Date();
   date_B.setFullYear( temp[3],  ( temp[2] -1 ) , temp[1] );
 
 // Datum A muss vor Datum B liegen
  if( date_A < date_B ) {
     proceed  = true;
  }
}
// Ende der Pruefung - nun Entscheidung...
alert( proceed ); // Test Ausgabe
...

Dann funktioniert diese Prüfung in allen Browsern. Aber warum? Wieso verhält sich der gleiche reguläre Ausdruck bei zwei Methoden so unterschiedlich? Steven Levithan beschreibt in seinem Artikel JavaScript split Bugs: Fixed! gleich eine ganze Reihe von Fehlern (die nicht nur den Internet Explorer betreffen), und liefert auch gleich eine Lösung, in dem er die ganze Methode überlagert. Auch wenn es in meinem Fall ausgereicht hat, die Methode einfach zu wechseln - dieses Verhalten bleibt ein Mysterium. Bedeutet es doch, dass da unterschiedliche Verabeitungsprozesse ablaufen. Wenn man bedenkt, dass reguläre Ausdrücke für viele ohnehin ein Buch mit sieben Siegeln darstellt, wie passend ist es da, dass es auch noch solche Sprach-Fehler gibt.

Ordnung ist das halbe Leben…

06. Juli 2011

Ordnung ist das halbe Leben
Geschachtelte geordnete Listen mit einer fortlaufenden Nummerierung in Form von 1., 1.1, 1.1.1 usw.? Seit CSS 2 lassen sich dank der neuen CSS-Counter auch solche Arten der Aufzählung realisieren. Für ältere Browser wie dem Internet Explorer 7 müssen dabei allerdings ein paar Hürden genommen werden. Dabei sind die CSS-Counter nicht auf Listenelemente beschränkt, da gäbe es eine ganze Reihe schöner Einsatzszenarien. Wahrlich kein brandneues Thema mehr, aber ein Blick lohnt trotzdem.

Geordnete Listen

CSS-Counter erlauben das Zählen von Instanzen bestimmter Elemente bzw. Selektoren. In Kombination mit den Pseudo-Elementen :before und :after lassen sich dann ganze neue Formen der Nummerierung erstellen. Die CSS-Counter sind dabei deutlich leistungsstärker als das alte HTML start Attribut, das ohnehin als deprecated gilt, und ausschließlich die Nummerierung einer Ebene bzw. eines Listenelementes beeinflusst.
Die Nummerierung mit Hilfe von CSS basiert setzt sich aus mehreren Eigenschaften und Funktionen zusammen:

  • counter-reset: Setzt einen oder mehrere Zähler zurück, ein Zähler ist dabei eine frei wählbare Variable. Optional lässt sich für jeden Zähler noch ein Anfangswert festlegen, der Standardwert ist 1.
  • counter-increment: Zählt einen oder mehrere Zähler hoch, auch hier kann man über einen zweiten optionalen integeren Wert die Zählweise beeinflussen (selbst negative Werte sind erlaubt).
  • content: Gibt mit Hilfe der Funktionen counter() oder counters() die Nummerierung aus.

Ein Beispiel

OL {
  counter-reset: item;
}
OL LI {
  display: block;
}
 
OL LI:before {
  content: counter(item) ". ";
  counter-increment: item;
}

Betrachtet man dieses erste Beispiel im Browser, so sieht das Ergebnis noch nicht anders aus, als eine ganz normale geordnete Liste (OL), der einzige Vorteil liegt darin, dass sich die Nummerierung nun erheblich besser gestalten lässt, ohne dass man weitere Tag Verschachtelungen vornehmen muss. Auf einen Nachteil sei an dieser Stelle auch gleich hingewiesen. In Sachen “Accessability” sind diese Lösungen negativ zu bewerten, denn über CSS generierter Content manipuliert die DOM nicht (siehe hier), so dass aktuelle Screen-Reader diese Informationen schlicht nicht ausgeben, die Liste hätte also keine Nummerierung mehr.
Für eine fortlaufende Nummerierung, die die übergeordneten Nummern übernimmt, kann man den CSS Code wie folgt verändern:

OL {
  counter-reset: item;
}
OL LI {
  display: block;
}
 
OL LI:before {
  content: counters(item, ".") ".";
  counter-increment: item;
}

Nun werden die Nummern der übergeordneten Listenelemente übernommen. Die Funktion counters() liefert im Gegensatz zu counter alle Instanzen eines Zählers gleichzeitig. Der zweite Parameter ist das Trennzeichen, mit dem diese Instanzen verkettet werden. Doch Vorsicht mit dieser Syntax. Sie führt im Internet Explorer 7 dazu, dass alle folgenden CSS Notationen nicht mehr interpretiert werden, selbst wenn diese in separaten Stylesheet Dateien abgelegt sind. Anscheinend bringt die Notation der Anfürhrungszeichen innerhalb der Klammern den CSS Interpreter völlig durcheinander. Bedeutet, man kann wahlweise sicher stellen, dass der entsprechende Code projektweit wirklich immer an letzter Stelle kommt, oder man nutzt die Funktion counter anstatt counters – wobei diese Lösung den CSS-Code abhängig von der möglichen Verschachtelungstiefe deutlich vergrößern kann:

/* 1. Ebene */
BODY OL {
  counter-reset: level1;
  list-style-type: none;
}
 
OL LI:before {
  content: counter(level1) ". ";
  counter-increment: level1;
}
 
/* 2. Ebene */
OL LI OL {
  counter-reset: level2;
}
 
OL LI OL LI:before {
  content: counter(level1) "." counter(level2) " ";
  counter-increment: level2;
}
 
/* 3. Ebene */
OL LI OL LI OL {
  counter-reset: level3;
}
 
OL LI OL LI OL LI:before {
  content: counter(level1) "." counter(level2) "." counter(level3) " ";
  counter-increment: level3;
}
...
Seite 1 von 212