Tim Tepaße: Verschachtelte Daten fürs Sortieren vorbereiten

Beitrag lesen

Dieses extrem verdichtete Beispiel ...

~~~javascript var histogram = "LinkA=4; LinkB=13; LinkC=21";

histogram
    .split("; ")
    .map(function (str) {
      var record = str.split("=");
      return {
        "link"  : record[0],
        "count" : Number(record[1])
      };
    })
    .sort(function (a, b) {
      return b.count - a.count;
    })
    .slice(0, 3)
    .forEach(function (record, index) {
      console.log(  String(index+1)
                  + " - "
                  + record.link
                  + " - "
                  + String(record.count));
    });

  
... ergibt diese Ausgabe:  
  
  1 - LinkC - 21  
  2 - LinkB - 13  
  3 - LinkA - 4  
  
Allerdings ist das nun sehr verdichtet und funktioniert auch nur in moderneren Browsern. Ich drösel das mal auf und gebe konventionellere Beispiele.  
  
Im wesentlichen geht es hier um das Umwandeln von verschachtelten Datentypen. Die Variable `histogram`{:.language-javascript} ist ein String. Strings kann man nicht gut mit Bordmitteln sortieren, also wird der String in ein Array umgewandelt. Strings haben die Methode [split()](http://de.selfhtml.org/javascript/objekte/string.htm#split), die einen String an bestimmten Stellen aufteilt und die Teile innerhalb eines Arrays zurück gibt.  
  
  `var arrayOfStrings = histogram.split("; ");`{:.language-javascript}  
  
... spaltet Deinen String immer dort, wo ein Semikolon und ein Leerzeichen auftauchen und gibt dieses zurück:  
  
  `["LinkA=4", "LinkB=13", "LinkC=21"]`{:.language-javascript}  
  
Das Problem bei diesem Array ist, dass es immer noch Strings enthält und die URLs und die Klickraten immer noch in diesem String zusammenhängen und die Klickraten auch noch als Strings und nicht als Zahlen vorliegen. Man muss also weiterarbeiten. So erklärt das auch die leicht ungewöhnliche Schreibweise im obigen Beispiel, die mit den jeweiligen Methodenaufrufen in der nächsten Zeile. Das ist ein verbundenes Statement, das immer auf den Rückgabewert der vorherigen Methode operiert. Konventionell sähe das so aus:  
  
  
  ~~~javascript
var arrayOfObjects = [],  
      str, arrayOfTwoStrings, link, countAsString, count, newObj;  
  
  for (var index = 0, end = arrayOfStrings.length; index < end; index++) {  
  
    // aktueller String aus dem Array holen  
    str = arrayOfStrings[index];  
  
    // Wieder aufsplitten, diesmal anhand des Gleichheitszeichens  
    arrayOfTwoStrings = str.split("=");  
  
    // Link aus dem neuen Array rausholen  
    link = arrayOfTwoStrings[0];  
  
    // dito die Klickrate  
    countAsString = arrayOfTwoStrings[1];  
  
    // ... die aber noch in eine Zahl statt eines Strings umgewandelt werden sollte  
    count = Number(countAsString);  
  
    // An die gleiche Stelle im Resultatsarray arrayOfObjects passt nur ein Ding  
    // also erstelle ich ein neues Objekt  
    newObj = {};  
  
    // ... weise diesem den Link zu  
    newObj.link = link;  
  
    // ... und die Klickrate  
    newObj.count = count;  
  
    // ... und packe es in das neue Array:  
    arrayOfObjects[index] = newObj;  
  }

Ok, das ist eklig ausführlich, dies ist gleichwertig:

~~~javascript var arrayOfObjects = [],
      str, record, newObj;

for (var index = 0, end = arrayOfStrings.length; index < end; index++) {

// aktueller String aus dem Array holen
    str = arrayOfStrings[index];

record = str.split("=");
    newObj = {
      "link"  : record[0],
      "count" : Number(record[1])
    };

// ... und das neu erstellte Objekt wieder in das neue Array packen
    arrayOfObjects[index] = newObj;
  }

  
Du siehst in der Mitte, dass die eigentliche Arbeit nur 5 Zeilen sind. Dieses Muster – einen Codeschnipsel für alle Elemente eines Arrays anwenden und in ein neues oder gleiches Array packen – kommt sehr oft vor. Deswegen gibt es in vielen Programmiersprachen eine Funktion oder Methode namens map, so auch inzwischen in modernen Browsern in Javascript als [Array.prototype.map](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map), d.h. als Methode von Array-Objekten. Map wird eine Funktion als Callback übergeben und führt diese Funktion für jedes Element des Arrays aus. Dieser Funktion wird das Element übergeben, der Rückgabewert der Funktion kommt dann wieder ins Array. Array.prototype.map existiert nur in modernen Browsern, dort kann man das dann so schreiben ...  
  
  ~~~javascript
function convert (element) {  
    var record = str.split("=");  
    return {  
      "link"  : record[0],  
      "count" : Number(record[1])  
    };  
  }  
  
  var arrayOfObjects = arrayOfStrings.map(convert);

... und muss sich nicht mit Schleifen rumschlagen. Oder eben als anonymes Funktionsobjekt wie ganz oben. Jetzt sind die Daten endlich so zubereitet, dass man sie nutzen kann. Die Variable arrayOfObjects zeigt jetzt auf diese Datenstruktur:

~~~javascript [ { "count" : 4, "link" : "LinkA"},
    { "count" : 13, "link" : "LinkB"},
    { "count" : 21, "link" : "LinkC"} ]

  
Ein Array mit einem Objekt-Element für jeden Datensatz, ein Objekt besteht aus dem Link als String und der Klickrate als echte Zahl. Jetzt kann sortiert werden:  
  
  ~~~javascript
function compare (a, b) {  
    // Hier werden nur die Klickraten-Zahlen zweier Objekte verglichen  
    // Und weil Du ab- statt aufsteigend sortieren willst, wird b von a  
    // statt wie sonst a von b subtrahiert  
    return b.count - a.count;  
  }  
  
  var sortedArray = arrayOfObjects.sort(compare);

Dort hätten wir dann diese Struktur:

~~~javascript [ { "count" : 21, "link" : "LinkC"},
    { "count" : 13, "link" : "LinkB"},
    { "count" : 4, "link" : "LinkA"} ]

  
Das hier ist im Beispiel eigentlich unnötig, weil Du schon drei Einträge hast. aber kann man gleich auf die Top 3 limitieren mit der Array-Methode [.slice()](http://de.selfhtml.org/javascript/objekte/array.htm#slice),die ein Teilarray zurück gibt, hier drei Elemente vom Index 0 an gezählt:  
  
  `var top3 = sortedArray.slice(0, 3);`{:.language-javascript}  
  
Und jetzt muss man das nur noch ausgeben, klassisch mit einer Schleife:  
  
  ~~~javascript
for (var index = 0, end = top3.length, element; index < end; index++) {  
  
    // aktuelles Element aus dem Array holen  
    element = top3[index];  
  
    // Und was damit tun, inkl. Typumwandlung  
    console.log(  String(index+1)  
                + " - "  
                + element.link  
                + " - "  
                + String(element.count));  
  }

Du siehst hier wieder ein Muster wie bei Map oben. In modernen Browsern gibt es bei Arrays die Methode .forEach(), die wieder mit einer Funktion als Callback arbeitet. So verwende ich die dann oben. Diese Methoden führen für einen auch den null-basierten Index, wie Du siehst.

console.log ist für Entwickler eine nettere Ausgabe in Firebug oder Safaris/Chromes Web Inspector, in deren interaktiver JS-Konsole man so etwas leicht ausprobieren kann.

...

Würde ich mehr die Schleifen oder mehr Methoden wie map/forEach verwenden? Ich mag letzten Stil mehr; je mehr man sich dran gewöhnt, desto lieber wird der Stil einem. Aber ich würde das wohl nicht so verdichtet als einzigen Ausdruck schreiben wie oben sondern hier und da wohl eine Zwischenvariable opfern. Und für ältere Browser würde ich Methoden map, each, filter, etc entweder selber nachrüsten oder aber aus einer Bibliothek bekommen wie z.B. jQuery.each und jQuery.map. Über diese Problemmuster stolpert man nämlich immer wieder.

--
Manchmal hab ich diese dämliche Lust auf zu ausführliche Tutorials ...