Tim Tepaße: Javascript diskriminiert nicht-(hexa-)dezimale Zahlen!

Beitrag lesen

Hallo Mathias, Steve,

Ich habe jetzt eine andere Lösung gefunden, trotzdem hätte ich Interesse warum die führenden Nullen abgeschnitten werden.

Erklärungen für sehr abseitige Themen geben wir doch immer gerne, besonders wenn die Antworten noch abwegiger sind.

Die Antwort liegt tief im ECMAScript-Standard, auf den Javascript aufbaut. Im wesentlichen läuft es darauf hinaus, dass Java- bzw EMCAScript gerne Dezimalschreibweise für seine Zahlen hätte, aus was für Gründen auch immer.

Und ist dir auch bewusst, dass dies dezimal „8“ entspricht, da du durch die führenden Nullen die Interpretation als Oktalzahlen erzwungen hast?

Prinzipiell zur Oktaldarstellung: Die ECMASCript-Spezifikation hatte anscheinend mal früher Unterstützung für Oktalzahlen, aber in der aktuellen dritten Edition wurde das rausgenommen:

B.1 Additional Syntax

Past editions of ECMAScript have included additional syntax and semantics for
  specifying octal literals and octal escape sequences. These have been removed
  from this edition of ECMAScript. This non-normative annex presents uniform
  syntax and semantics for octal literals and octal escape sequences for
  compatibility with some older ECMAScript programs.

Jemandem, der ECMA/Javascript implementiert, steht es also frei, Oktalzahlen zusätzlich einzubauen. Die meisten Implementierungen tun dieses. Aber nach den Regeln von ECMAScript, die etwas verworren sind.

Konkret hast Du ja diesen auszuwertenden Ausdruck:

"000" | "010"

Dir liegen also die Binärzahlen als Zeichenketten vor. Das ist hier relevant. Gucken wir mal, wie die Abfolge des Bitweisen Oders in ECMAScript definiert ist:

1. Evaluate A.
  2. Call GetValue(Result(1)).
  3. Evaluate B.
  4. Call GetValue(Result(3)).
  5. Call ToInt32(Result(2)).
  6. Call ToInt32(Result(4)).
  7. Apply the bitwise operator @ to Result(5) and Result(6). The result is a
     signed 32 bit integer.
  8. Return Result(7).

Sprich, es gibt ein definiertes Vorgehen, wie ECMAScript-Implementierungen Bitweise Operatoren in Ausdrücken anzuwenden hat. Die Schritte 1 bis 4 sind klassisch das Auswerten der Ausdrücke links und rechts des "|", also vernachlässigbar, weil da nichts komplizierteres steht. Die Schritte 5 und 6 sind die Interessanten, dort geschieht die implizite Typumwandlung der Variablen von Javascript. Konkret wird dort eine nicht allgemein verfügbare Funktion ToInt32() aufgerufen, die nach bestimmten Regeln aus anderen Objekte ein 32 Bit langes Integer macht auf den dann das Bitweise Oder angewendet wird.

ToInt32() ist natürlich auch definiert, Schritt 1 ist in dieser Definition relevant, die anderen Schritte sind nur dafür da, dass da auch bestimmt eine 32 bit langes Integerzahl rauskommt. Schritt 1 ruft eine weitere interne, nur zur automatischen Typumwandlung gedachte Funktion auf:

1. Call ToNumber on the input argument.

Die Funktion ToNumber() ist auch definiert, für die Anwendung auf Strings sogar besonders. Ich erspare die Details, im wesentlichen läuft es darauf hinaus, die Zeichen des String zu überprüfen, ob dieser einer Miniatur-Grammatik, der Produktion „StringNumericLiteral“ entspricht und diese dann in eine tatsächliche Zahl umzuwandeln, wenn nicht, wird NaN (Not a Number) zurückgegeben.

Diese Produktion für Zahlen in Strings ist nicht die gleiche Produktion für Zahlen direkt im Programmcode, die sogenannten Literale („NumericLiteral“), aber in beiden Abschnitten in der ECMAScript-Spezifikation werden Strings in Zahlen umgewandelt. Bei der Produktion „NumericLiteral“ (Zahlen direkt im Programmcode) wird die führende Null nach dem vorhin zitierten Anhang B als Indikator für eine Oktalzahl genommen, 010 ist also die Zahl 8.

Im Abschnitt zu „StringNumericLiteral“ gibt es jedoch keine durch Anhang B erweiterte Funktionalität für Oktalzahlen. Dort werden Zahlen mit führender Null als Dezimalzahlen erkannt und bei der Umwandlung in eine tatsächliche Zahl als solche interpretiert, die führenden Nullen sind also irrelevant.

Um das mal als Beispiel klar zu machen, kopiere dieses mal in Deine Adressleiste und führe es aus:

javascript:alert(000 | 010);

In meinen Browsern steht im Popup eine 8. Weil: 000 ist wegen der führenden Null eine Oktalzahl, ist aber eine Null im Zahlen-Sinne. 010 ist in der Produktion „NumericLiteral“ wegen der führenden Null eine Oktalzahl, 10 in oktaler Schreibweise ist eine 8 (in dezimaler Schreibweise). 0 mit bitweisen Oder verknüpft mit 8 ergibt wieder eine 8. Nun die andere Variante:

javascript:alert("000" | "010");

Ich erhalte hier eine 10. Weil: 000 ist dezimal interpretiert eine 0, 010 ist in der Produktion StringNumericLiteral eine 10; hier wird die Zeichenkette eben nicht als Oktalzahl interpretiert. 0 und 10 verknüpft mit bitweisem Oder ergeben natürlich eine 10.

Warum das so ist? Keine Ahnung. Ich vermute einfach die angebliche Optionalität der Oktalzahlen in ECMAScript hat da Lücken in der Spezifikation hinterlassen, dazu kommt, dass ECMAScript eindeutig Dezimal- und Hexadezimalzahlen bevorzugt.

Noch eine Merkwürdigkeit: Bei expliziter Typumwandlung mit parseInt(string) darf  laut Spezifikation die führende Null von Zahlen im String wieder eine Oktalzahl andeuten. Es bleibt nur der Schluss: Führende Nullen bei Zahlen in Javascript können Merkwürdigkeiten hervorrufen.

Zu Deinem Problem: Du hast inzwischen ja schon gemerkt, dass es so etwas wie Binärzahlen in Javascript nicht gibt, es gibt nur den Datentyp Number, der für eine abstrakte Zahl unabhängig von der Notation steht. Operationen wie Bitweises Oder werden immer nur auf Zahlen angewandt, dabei verlierst Du zwangsläufig Deine führenden Nullen. Schlimmer noch, Deine Strings werden als Dezimalzahlen geparst. Bei einem Bitweisen Oder der Binärzahlen "000" (Dezimal: 0) und "010" (Dezimal: 2) kommt nur zufälligerweise eine passende angebliche Binärzahl "10" (Dezimal: 2) raus, in Wirklichkeit meint Javascript die Dezimalzahl "10". Und schon hast Du ein Problem. Denn diese zufällige Übereinstimmung ist nur sehr selten vorhanden.

Das heisst, für Dein Problem, sollte es noch bestehen, brauchst Du eine Lösung, die das Bitweise Oder auf die einzelnen Zeichen des Binärstrings anwendet. Oder aber Du konvertierst die Binärstrings explizit zu abstrakten Zahlen, führst darauf das Bitweise Oder aus (denn „signed 32 Bit Integers“ besitzen größtenteils die gleiche Repräsentation wie Deine Bitstrings) und konvertierst das dann wieder zu Bitstrings. Netterweise besitzt Javascript dafür passende Funktionen.

parseInt() besitzt ein optionales zweites Attribut, dass die Basis des Zahlenbereiches der im String enthaltenen Zahl angibt. Klingt kompliziert, ist es aber nicht.

parseInt("010", 2) ergibt die Zahl 2. Du kannst dies nutzen, um Deine Bitstrings explizit zu den passenden Zahlen zu casten.

Zahl-Objekte in Javascript haben die Methode toString(), auch diese hat einen optionalen Parameter, der die Basis des gewünschten Zahlenbereiches angibt. Kann  man also gut benutzen, um aus einer Zahl wieder einen einen Bitstring zu kriegen.

Ein angepasstes Bitweises Oder für Bitstrings sähe also so aus:

~~~javascript function myBitwiseOr (str_a, str_b) {
      return (parseInt(str_a, 2) | parseInt(str_b, 2)).toString(2);
  }

  
Wenn Dir sehr an den führenden Nullen liegt, dann solltest Du dafür Sorge tragen, dass das zurückgegebene Ergebnis links mit Nullen in der passenden Länge aufgefüllt wird:  
  
  ~~~javascript
function pimpedBitwiseOr (str_a, str_b) {  
      length = Math.max(str_a.length, str_b.length);  
      bits = (parseInt(str_a, 2) | parseInt(str_b, 2)).toString(2);  
      str = "";  
      for (i = 0; i < (length - bits.length); i++) {  
          str += "0";  
      }  
      return str + bits;  
  }

Tim