Zeichen zählen mit Rust

Dieses Programm zeigt, wie man Zeichen in einer Datei zählt, sie nach verschiedenen Kriterien sortiert und es in einer Pipeline mit Befehlen wie “cat” (Linux) oder “type” (Windows) verwenden kann.

Es verwendet HashMaps, um die Zeichen zu zählen und zu sortieren, die clap-Bibliothek, um Kommandozeilenargumente zu verarbeiten und setzt die atty-Bibliothek ein, um zu erkennen, ob die Standardeingabe ein Terminal ist, um Pipelining zu ermöglichen.

Darüber hinaus erklärt es detailliert, wie der Algorithmus funktioniert, der Zeichen zählt, indem es Zeile für Zeile durch den eingegebenen Text geht, jeden Buchstaben zählt und die Ergebnisse in einer HashMap speichert und sortiert nach verschiedenen Kommandozeilenargumenten ausgibt.

Cargo.toml

[package]
name = "charactercounter"
version = "0.0.0"
edition = "2021"
description = "Zählt Zeichen in einer Textdatei oder von der Standardeingabe"

[dependencies]
clap = "3.2.23"
atty = "0.2.14"

main.rs

use std::collections::HashMap;
use std::io::{self, BufRead};
use clap::{App, Arg};
use atty::Stream;
use std::fs::File;

fn main() {
    // Definieren der Kommandozeilenargumente mit clap
    let matches = App::new(env!("CARGO_PKG_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .about(env!("CARGO_PKG_DESCRIPTION"))
        .arg(Arg::new("datei")
            .value_name("DATEI")
            .help("Die Datei, die analysiert werden soll.")
            .required(false))
        .arg(Arg::new("sortierung")
            .short('s')
            .long("sortierung")
            .value_name("SORTIERUNG")
            .help("Die Sortierreihenfolge (zeichen|anzahl). Standardmäßig: zeichen.")
            .takes_value(true)
            .required(false))
        .get_matches();

    // Überprüfen, ob die Standardeingabe ein Terminal ist
    let ist_terminal = atty::is(Stream::Stdin);

    // Wenn keine Datei angegeben wurde und die Standardeingabe ein Terminal ist, zeige einen Hilfetext an
    if matches.value_of("datei").is_none() && ist_terminal {
        println!("Datei als Parameter angeben oder Programm mit --help aufrufen");
        return;
    }

    // Kommandozeilenargumente einlesen
    let dateiname = matches.value_of("datei");
    let standard_sortierreihenfolge = String::from("zeichen");
    let sortierreihenfolge = matches.value_of("sortierung").unwrap_or(&standard_sortierreihenfolge);

    // Wenn keine Datei angegeben wurde oder die Datei "-" ist, lesen Sie von stdin
    if dateiname.is_none() || dateiname.unwrap() == "-" {
        let eingabe = io::stdin();
        let leser = eingabe.lock();

        // Verarbeiten Sie den Text von stdin
        text_verarbeiten(leser, sortierreihenfolge);
        
    } else {
        // Ansonsten versuchen Sie, die angegebene Datei zu öffnen und den Text daraus zu lesen
        if let Some(dateiname) = dateiname {
            let datei = match File::open(dateiname) {
                Ok(datei) => datei,
                Err(_) => {
                    eprintln!("Fehler beim Öffnen der Datei: '{}'", dateiname);
                    return;
                }
            };

            let leser = io::BufReader::new(datei);

            // Verarbeiten Sie den Text aus der Datei
            text_verarbeiten(leser, sortierreihenfolge);
        }
    }
}

fn text_verarbeiten<T: BufRead>(leser: T, sortierreihenfolge: &str) {
    // Erstellen einer HashMap, um die Zeichen und ihre Anzahl zu zählen
    let mut zeichen_zähler: HashMap<char, usize> = HashMap::new();

    // Iterieren Sie über jede Zeile und zählen Sie die Zeichen
    for zeile in leser.lines() {
        if let Ok(zeile) = zeile {
            for zeichen in zeile.chars() {
                // Inkrementieren Sie den Zähler des Zeichens in der HashMap
                *zeichen_zähler.entry(zeichen).or_insert(0) += 1;
            }
        }
    }

    // Konvertieren Sie die HashMap in einen Vektor von Tupeln
    let mut sortierte_zeichen_zählungen: Vec<(char, usize)> = zeichen_zähler.into_iter().collect();

    // Sortieren je nach gewählter Option
    match sortierreihenfolge {
        "zeichen" => sortierte_zeichen_zählungen.sort_by(|a, b| a.0.cmp(&b.0)),  // Sortiere nach Zeichen
        "anzahl" => sortierte_zeichen_zählungen.sort_by(|a, b| a.1.cmp(&b.1).reverse()),  // Sortiere nach Anzahl, absteigend
        _ => {
            eprintln!("Ungültige Sortieroption. Verwenden Sie 'zeichen' oder 'anzahl'.");
            return;
        }
    }

    // Geben Sie die sortierte Liste der benutzten Zeichen, deren Unicode-Codepunkt und die Anzahl aus
    for (z, zähler) in sortierte_zeichen_zählungen {
        // Generiere den Hex-Wert des Unicode-Codepunkts
        let hexadezimaler_codepunkt = format!("{:X}", z as u32);

        // Gib Zeichen, Hex-Codepunkt, Codepunkt und Anzahl aus, getrennt durch ein Tab
        println!(
            "{}\t0x{}\t{}\t{}",
            z,
            hexadezimaler_codepunkt,
            z as u32,
            zähler
        );
    }
}

Pipeline

Pipelining ist eine Methode, um verschiedene Befehle auf Daten nacheinander anzuwenden, wobei das Ergebnis eines Befehls direkt an den nächsten Befehl übergeben wird.

Windows

type textdatei.txt | charactercounter.exe

Linux

cat textdatei.txt | charactercounter

Algorithmus

Die Funktion text_verarbeiten ist die Funktion, die die Zeichen zählt, sortiert und ausgibt.

fn text_verarbeiten<T: BufRead>(leser: T, sortierreihenfolge: &str) {

Dies ist die Definition einer Funktion namens text_verarbeiten.

<T: BufRead>

Dieser Teil der Funktionssignatur zeigt an, dass die Funktion generisch ist und mit verschiedenen Typen von T arbeiten kann. In Rust kann man generische Funktionen schreiben, die für verschiedene Datentypen verwendet werden können. In diesem Fall verwenden wir T, um einen Typ zu repräsentieren, der den Textstrom darstellt, den wir verarbeiten werden. Das <T: BufRead> bedeutet, dass T ein Typ sein muss, der das BufRead-Trait implementiert. Traits sind in Rust eine Möglichkeit, gemeinsame Verhaltensweisen für verschiedene Typen zu definieren.

(leser: T, sortierreihenfolge: &str)

Dies sind die Parameter, die die Funktion erwartet. leser ist der Eingabestrom, von dem wir Text lesen werden, und sortierreihenfolge ist eine Zeichenkette (String), die angibt, wie wir den Text sortieren wollen.

let mut zeichen_zähler: HashMap<char, usize> = HashMap::new();

Hier wird eine neue leere HashMap erstellt, die wir zeichen_zähler nennen. Eine HashMap ist eine Datenstruktur, die Schlüssel-Wert-Paare speichert. In diesem Fall werden Zeichen (char) die Schlüssel sein, und die Anzahl ihrer Vorkommen (usize) wird der Wert sein. Wir erstellen es als mut, was bedeutet, dass wir es ändern können, nachdem es erstellt wurde.

for zeile in leser.lines() {

Dies ist eine Schleife, die durch jede Zeile des Texts im Eingabestrom leser iteriert. Hier verwenden wir die Methode lines(), die auf leser aufgerufen wird, um Zeilen des Texts zu liefern, eine Zeile nach der anderen.

if let Ok(zeile) = zeile {

Dies ist eine Art, wie Rust mit Fehlern umgeht. Wir versuchen, die Zeile zu lesen, aber es könnte Fehler geben, wenn wir auf das Ende des Textstroms stoßen oder auf andere Probleme. Diese Zeile prüft, ob das Lesen erfolgreich war (Ok). Wenn ja, fahren wir fort, andernfalls überspringen wir diese Zeile.

for zeichen in zeile.chars() {

Dies ist eine Schleife, die durch jeden Buchstaben (Zeichen) in der aktuellen Zeile zeile iteriert. Wir verwenden die Methode chars(), um die Zeichen in der Zeile zu erhalten.

*zeichen_zähler.entry(zeichen).or_insert(0) += 1;

Hier zählen wir die Anzahl der Vorkommen jedes Zeichens in der HashMap zeichen_zähler. Das entry()-Methode gibt uns Zugriff auf das Eintrag für das aktuelle Zeichen. Wenn es bereits existiert, wird es zurückgegeben. Wenn nicht, wird ein neues mit dem Wert 0 erstellt. Dann erhöhen wir den Wert um 1.

Der Stern (*) in *zeichen_zähler.entry(zeichen).or_insert(0) ist ein Entfernen der Referenz oder eine Dereferenzierung. Der Stern wird verwendet, um auf die tatsächlichen Werte in der HashMap zuzugreifen, wenn dies erforderlich ist. In Rust sind Container, wie z.B. HashMap, so konzipiert, dass sie Werte halten, die auf dem Heap (dynamischer Speicherbereich) gespeichert sind, und diese Werte sind normalerweise über einen Zeiger (Referenz) zugänglich.

Wenn wir * verwenden, greifen wir auf den tatsächlichen Wert zu, auf den die Referenz verweist, anstatt die Referenz selbst zu verwenden. In diesem Fall bedeutet es, dass wir auf den Wert zugreifen, den entry(zeichen) in der HashMap repräsentiert.

zeichen_zähler.entry(zeichen)

ruft einen Eintrag (Entry) in der HashMap für das Zeichen zeichen ab. Ein Eintrag ist eine Art von Verweis auf den Wert, der diesem Zeichen in der HashMap zugeordnet ist.

.or_insert(0)

ist eine Methode, die auf das Entry angewendet wird. Diese Methode überprüft, ob der Eintrag bereits existiert. Wenn der Eintrag existiert, gibt sie eine Referenz auf den Wert zurück. Wenn der Eintrag nicht existiert, fügt sie den Eintrag mit dem angegebenen Standardwert (in diesem Fall 0) hinzu und gibt eine Mutable Reference auf diesen Standardwert zurück.

Schließlich verwenden wir *, um auf den Wert zuzugreifen, der sich hinter der Mutable Reference verbirgt. Das Ergebnis dieses Ausdrucks ist also der Wert, der dem Zeichen zeichen in der HashMap zugeordnet ist, nachdem alle erforderlichen Operationen ausgeführt wurden.

let mut sortierte_zeichen_zählungen: Vec<(char, usize)> = zeichen_zähler.into_iter().collect();

Hier erstellen wir einen Vektor von Tupeln (Paare von Werten), der die sortierte Liste der Zeichen und ihrer Vorkommen enthält. Wir verwenden die into_iter()-Methode, um die HashMap in einen Iterator zu konvertieren, und die collect()-Methode, um die Ergebnisse in einen Vektor zu sammeln.

match sortierreihenfolge {
    "zeichen" => sortierte_zeichen_zählungen.sort_by(|a, b| a.0.cmp(&b.0)),
    "anzahl" => sortierte_zeichen_zählungen.sort_by(|a, b| a.1.cmp(&b.1).reverse()),
    _ => {
        eprintln!("Ungültige Sortieroption. Verwenden Sie 'zeichen' oder 'anzahl'.");
        return;
    }
}

Dieser Teil verwendet match, um die sortierreihenfolge zu überprüfen und die sortierte Reihenfolge entsprechend zu ändern. Wenn sortierreihenfolge gleich “zeichen” ist, sortieren wir nach Zeichen, wenn sie gleich “anzahl” ist, sortieren wir nach Anzahl (in umgekehrter Reihenfolge). Wenn es etwas anderes ist, zeigen wir eine Fehlermeldung und kehren aus der Funktion zurück.

In diesem Codeausschnitt handelt es sich um ein Match-Ausdruck in Rust, der je nach dem Wert von “sortierreihenfolge” unterschiedliche Aktionen ausführt. Lassen Sie uns die Bedeutung der Schlüsselwörter und Symbole klären:

  • "zeichen" und "anzahl": Dies sind Zeichenketten-Literale, die die möglichen Werte für die “sortierreihenfolge” repräsentieren. Wenn “sortierreihenfolge” einer dieser Zeichenketten entspricht, wird der entsprechende Fall ausgeführt.
  • sortierte_zeichen_zählungen: Dies ist ein Vektor von Tupeln, der die Zeichen und ihre Anzahl enthält.
  • sort_by: Dies ist eine Methode, die auf Vektoren in Rust verfügbar ist. Sie ermöglicht es, die Elemente des Vektors nach einem bestimmten Kriterium zu sortieren.
  • |a, b|: Dies ist eine Closure (eine anonyme Funktion), die als Argument an sort_by übergeben wird. Die Closure erhält zwei Argumente, a und b, die jeweils ein Tupel aus dem Vektor repräsentieren, das verglichen wird.
  • a.0.cmp(&b.0): In dieser Zeile wird das erste Element (Index 0) der Tupel a und b verglichen. Dieser Vergleich erfolgt mit der Methode cmp, die in Rust verwendet wird, um die Reihenfolge zweier Elemente zu bestimmen. Das Ergebnis ist entweder Less, Equal oder Greater, je nachdem, ob a kleiner, gleich oder größer als b ist. In diesem Fall wird nach den Zeichen (char) sortiert.
  • a.1.cmp(&b.1).reverse(): Hier wird das zweite Element (Index 1) der Tupel a und b verglichen, und dann wird das Ergebnis umgekehrt. Dies bedeutet, dass die Sortierung nach der Anzahl der Zeichen in absteigender Reihenfolge erfolgt.

Das Zeichen & in Ausdrücken wie &b.0 und &b.1 steht für eine sogenannte Referenz in Rust. Es wird verwendet, um auf den Wert eines Objekts (in diesem Fall ein Tupel) verweisen, anstatt eine Kopie dieses Werts zu erstellen. Dies ist eine wichtige Eigenschaft in Rust, um sicherzustellen, dass Daten effizient verwaltet und gleichzeitig sicher genutzt werden können.

Genauer gesagt:

  • &b.0 bedeutet, dass Sie auf das erste Element (Index 0) des Tupels b verweisen. Es gibt Ihnen eine “Referenz” auf dieses Element, ohne eine Kopie davon zu erstellen. In diesem Fall wird angenommen, dass das erste Element von Tupel b ein Wert des Typs ist, auf den eine Referenz erzeugt werden kann, z.B. ein char.
  • &b.1 bedeutet dasselbe, jedoch für das zweite Element (Index 1) des Tupels b. Es gibt Ihnen eine Referenz auf das zweite Element, ohne eine Kopie zu erstellen. Hier wird angenommen, dass das zweite Element ein Wert ist, auf den ebenfalls eine Referenz erstellt werden kann, wie zum Beispiel ein usize (eine ganze Zahl).

Der Hauptgrund, warum Referenzen verwendet werden, ist, um Speicherplatz zu sparen und effizientere Operationen durchzuführen, insbesondere wenn es um größere Datenstrukturen geht. Wenn Sie eine Referenz auf ein Objekt haben, arbeiten Sie direkt mit dem Originalobjekt, anstatt eine Kopie davon zu erstellen. Dies ist wichtig, um das Speichermanagement in Rust sicherzustellen und gleichzeitig die Leistung zu optimieren.

Das Ganze dient dazu, die Elemente des Vektors sortierte_zeichen_zählungen basierend auf dem Wert von “sortierreihenfolge” zu sortieren. Wenn “sortierreihenfolge” den Wert "zeichen" hat, erfolgt die Sortierung nach den Zeichen selbst. Wenn “sortierreihenfolge” den Wert "anzahl" hat, erfolgt die Sortierung nach der Anzahl der Zeichen, und das Ergebnis wird in absteigender Reihenfolge sortiert. Wenn “sortierreihenfolge” einen anderen Wert hat, wird eine Fehlermeldung ausgegeben.

for (z, zähler) in sortierte_zeichen_zählungen {
    let hexadezimaler_codepunkt = format!("{:X}", z as u32);
    println!("{}\t0x{}\t{}\t{}", z, hexadezimaler_codepunkt, z as u32, zähler);
}

Schließlich durchlaufen wir den sortierten Vektor von Zeichen und ihren Vorkommen und drucken sie aus. Wir erstellen auch den Hexadezimalwert des Unicode-Codepunkts jedes Zeichens und zeigen ihn an.