Morsecode-Codierer / Decodierer mit Wiedergabefähigkeit

Aug 19 2020

Vor kurzem wollte ich einen Morse-Encoder / Decoder mit Wiedergabefähigkeit erstellen, das Programm muss java version >= 11ausgeführt werden.

Das Programm erfordert ein paar jars:

  • com.google.common.collect.BiMap

  • javazoom.jl.player.Player

Ich habe das BiMapaus folgendem Grund benutzt:

Eine Bimap (oder "bidirektionale Karte") ist eine Karte, die die Eindeutigkeit ihrer Werte sowie die ihrer Schlüssel bewahrt. Diese Einschränkung ermöglicht es Bimaps, eine "inverse Ansicht" zu unterstützen. Dies ist eine weitere Bimap, die dieselben Einträge wie diese Bimap enthält, jedoch umgekehrte Schlüssel und Werte. ref

Verwenden Sie wie viele Online-Morseübersetzer das Zeichen '/'oder a ',', um in den von mir verwendeten Raum übersetzt zu werden '\t'.

In Bezug auf die Struktur habe ich das Singleton-Entwurfsmuster verwendet. Damit der Benutzer eine begrenzte Anzahl von Objekten haben kann, muss kein Objekt zum Codieren / Decodieren erstellt werden, wenn es bereits vorhanden ist.

Das Programm bietet Folgendes:

  1. Flexibel kann es also aus der gewünschten Datenbank lesen.

  2. Kompatibel mit allen Arten von zulässigen CharSetJava-Backen (wenn Sie den richtigen Zeichensatz zum Lesen einer bestimmten Datei verwenden).

  3. Audiowiedergabe, um Menschen zu helfen, Morsecode durch Hören zu verstehen!.

  4. Möglichkeit, Ergebnisse über den gewünschten Pfad in eine Datei zu schreiben.

  5. Das Programm berücksichtigt Regex beim Lesen der Datenbankdatei, da der Regex als Trennzeichen zwischen dem tatsächlichen Buchstaben und der Folge von Punkten und Strichen fungiert.

Also hier ist der Code:

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.Player;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;

import static com.google.common.collect.Maps.unmodifiableBiMap;

/**
 * This class represents Encoder and Decoder for Morse code.
 * @author  Kazem Aljalabi.
 */
public final class Morse {

    private Path dataBaseFile;
    private BiMap<String, String> data;
    private Charset cs = StandardCharsets.UTF_8;
    private String charSeparationRegex = " ";

    //Singleton Pattern via Lazy Instantiation = private constructor + static object that will be created once!.
    private static Morse defaultObj, pathObj, objWithSeparator, objWithCharSet;

    /**
     * This Method creates a class instance of type {@link Morse} if not created before else return the already created object.
     * @return a class instance of type {@link Morse}.
     */
    public static Morse getInstance() {
        if (null == defaultObj)
            defaultObj = new Morse();
        return defaultObj;
    }

    /**
     * This Method creates a class instance of type {@link Morse} if not created before else return the already created object.
     * @param dataBaseFile the path to the database which contains the actual decoding and encoding table of the morse code.
     * @return a class instance of type {@link Morse} linked with a database of user's choice via a {@link Path}.
     */
    public static Morse getInstance(final Path dataBaseFile) {
        if (null == pathObj)
            pathObj = new Morse(dataBaseFile);
        return pathObj;
    }


    /**
     * This Method creates a class instance of type {@link Morse} if not created before else return the already created object.
     * @param dataBaseFile the {@link Path} to the database which contains the actual decoding and encoding table of the morse code.
     * @param separator the regex which will act as a separator between the actual letter and its representation in morse code.
     * @return a class instance of type {@link Morse} linked with database path and a separator.
     */
    public static Morse getInstance(final Path dataBaseFile, final String separator) {
        if (null == objWithSeparator)
            objWithSeparator = new Morse(dataBaseFile, separator);
        return objWithSeparator;
    }
    
    /**
     * This Method creates a class instance of type {@link Morse} if not created before else return the already created object.
     * @param dataBaseFile the path to the database which contains the actual decoding and encoding table of the morse code.
     * @param separator the regex which will act as a separator between the actual letter and its representation in morse code.
     * @param cs the {@link Charset} in which the database is written with.
     * @return a class instance of type {@link Morse} linked with the database with a specific path, charset, and separator.
     */
    public static Morse getInstance(final Path dataBaseFile, final String separator, final Charset cs) {
        if (null == objWithCharSet)
            objWithCharSet = new Morse(dataBaseFile, separator, cs);
        return objWithCharSet;
    }

    /**
     * @param dataBaseFile path to the new dataBaseFile to be set.
     */
    public void setDataBaseFile(Path dataBaseFile) {
        this.dataBaseFile = dataBaseFile;
        checkForDataBase();
    }

    /**
     * Constructor to create a class instance of type {@link Morse} with a default database called "Code.txt" placed in the same dir with the class.
     */
    private Morse() {
        dataBaseFile = Paths.get(Morse.class.getResource( "Morse.class" ).getPath()).toAbsolutePath().normalize().getParent().resolve("Code.txt");
        checkForDataBase();
    }


    /**
     * Constructor creates a class instance of type {@link Morse} with a custom database provided by the user via a valid path.
     * @param dataBaseFile the path to the database which contains the actual decoding and encoding table of the morse code.
     */
    private Morse(final Path dataBaseFile) {
        this.dataBaseFile = dataBaseFile;
        checkForDataBase();
    }

    /**
     * Constructor creates a class instance of type {@link Morse} with a custom database with a specific separator provided by the user via a valid path.
     * @param dataBaseFile the {@link Path} to the database which contains the actual decoding and encoding table of the morse code.
     * @param separator the regex which will act as a separator between the actual letter and its representation in morse code.
     */
    private Morse(final Path dataBaseFile, final String separator) {
        this (dataBaseFile);
        assert separator != null;
        if ( checkForRegexValidity(separator) && !separator.contains(".") && !separator.contains("_") ) //those are reserved to the morse code!
            this.charSeparationRegex = separator;
    }

    /**
     * Constructor creates a class instance of type {@link Morse} with a custom database with a specific separator provided by the user via a valid path.
     * The database file is written in a specific CharSet.
     * @param dataBaseFile the path to the database which contains the actual decoding and encoding table of the morse code.
     * @param separator the regex which will act as a separator between the actual letter and its representation in morse code.
     * @param cs the {@link Charset} in which the database is written with.
     */
    private Morse(final Path dataBaseFile, final String separator, final Charset cs) {
        this (dataBaseFile, separator);
        this.cs = cs;
    }


    /**
     * Method to check the existence of database path.
     */
    private void checkForDataBase () {
        if (!Files.exists(dataBaseFile))
            System.exit(1);
        data = unmodifiableBiMap(populateFromDataBase());
    }

    /**
     * Method to check if the separator provided by the user is a valid regex.
     * @param regex database separator provided by the user.
     * @return true if the regex is valid else false.
     */
    private boolean checkForRegexValidity (String regex) {
        PatternSyntaxException flag = null;
        try {
            Pattern.compile(regex);
        } catch (PatternSyntaxException exception) { flag=exception; }
        return flag == null;
    }

    /**
     * Method to populate the Database from the database {@link java.io.File}.
     * @return a {@link BiMap} which contains the encoding/decoding schema of the Morse code based on the database file.
     */
    private BiMap<String, String> populateFromDataBase () {
        List<String> encodingSchema = new ArrayList<>();

        try {
            encodingSchema = Files.readAllLines(dataBaseFile, cs);
        } catch (IOException e) { e.printStackTrace(); }

        //To prevent the empty of being inserted inside the Hash we need to filter it out!
        return encodingSchema.stream().filter(s -> !s.equals(""))
                .collect(Collectors.toMap(
                        e -> e.replaceAll(charSeparationRegex," ").strip().split("\\s+")[0]
                        ,  e -> e.replaceAll(charSeparationRegex," ").strip().split("\\s+")[1]
                        , (e1, e2) -> e2
                        , HashBiMap::create)
                );
    }

    /**
     * Method which will write a specific message to a given file.
     * @param data The data to be written to a file. the data can be an already encoded message or the decoded message of an already encoded message!.
     * @param resultsPath the path where the results would be written, if it doesn't exist it will be created.
     */
    public void writeResultsToFile (String data, Path resultsPath) {
        try {
            Files.writeString(resultsPath, data, StandardOpenOption.CREATE);
        } catch (IOException e) { e.printStackTrace(); }
    }

    /**
     * Method to decode a given Message based on the given database and the morse code logic.
     * @param message to be decoded assuming that the message contains only '_' and '.', assuming that the message given contains no foreign chars that don't exist in the database given.
     * @return a decoded version of the provided message.
     */
    public String decodeMessage(String message) {
        var builder = new StringBuilder();

        for (var str : message.strip().split("\t"))
            builder.append(decodeHelper(str)).append(" ");

        return builder.toString().strip();
    }

    /**
     * A helper method to decode One Word at a time.
     * @param word which consists of '_' and '.' which will be encoded accordingly to the given database.
     * @return a valid decoded word.
     */
    private StringBuilder decodeHelper (String word) {
        return Arrays.stream(word.split(" "))
                .collect(StringBuilder::new
                        , (builder, s) -> builder.append(data.inverse().getOrDefault(s, " "))
                        , StringBuilder::append
                );
    }

    /**
     * Method to encode a certain message based on the provided database.
     * @param message to be encoded assuming that the message given contains no foreign chars that don't exist in the database given.
     * @return an encoded version to the provided message which consists of only '_' and '.'.
     */
    public String encodeMessage (String message) {

        var builder = new StringBuilder();

        for (var str : message.toUpperCase().strip().split("")) {
            builder.append(data.getOrDefault(str, ""));
            if (!str.equals(" "))
                builder.append(" ");
            else
                builder.append("\t");//insert tap to tell when word ends!.
        }
        return builder.toString().strip();
    }

    /**
     * Method to play the actual sound of a certain message while being encoded.
     * @param data to be encoded.
     */
    public void encodeAndPlayAudio (String data) {
        var encoded = encodeMessage(data).split("\t");
        var tabsNumber = encoded.length-1;

        for (var c : encoded) {
            playAudio(c);

            if (tabsNumber-- > 0){
                System.out.print("\t");
                try { Thread.sleep(1000); } catch (InterruptedException ignored) {  }
            }
        }
        System.out.println();
    }

    /**
     * @param filename of the soundtrack to be played.
     */
    private void playMp3 (String filename) {
        try (var fis = new FileInputStream(Morse.class.getResource(filename).getPath())) {
            new Player(fis).play();
        } catch (IOException | JavaLayerException e) { e.printStackTrace(); }
    }

    /**
     * Method to decide which soundtrack will get played based on the current char.
     * @param encodeMessage which will be played.
     */
    private void playAudio (String encodeMessage) {
        for (var c : encodeMessage.strip().toCharArray()){
            if (c == '.')
                playMp3("di.mp3");
            else if (c == '_')
                playMp3("dah.mp3");

            System.out.print(c);
        }
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Morse morse = (Morse) o;
        return dataBaseFile.equals(morse.dataBaseFile) &&
                data.equals(morse.data);
    }

    @Override
    public int hashCode() { return Objects.hash(dataBaseFile, data); }

    @Override
    public String toString() {
        return "Morse{" +
                "dataBaseFile=" + dataBaseFile +
                ", data=" + data +
                '}';
    }
}

Datenbankbeispielcode.txt (kann natürlich auf Wunsch erweitert werden):

A   ._
B   _...
C   _._.
D   _..
E   .
F   .._.
G   __.
H   ....
I   ..
J   .___
K   _._
L   ._..
M   __
N   _.
O   ___
P   .__.
Q   __._
R   ._.
S   ...
T   _
U   .._
V   ..._
W   .__
X   _.._
Y   _.__
Z   __..
1   .____
2   ..___
3   ...__
4   ...._
5   .....
6   _....
7   __...
8   ___..
9   ____.
0   _____

Das Benutzerhaupt würde so aussehen :

public class Main {

    public static void main(String[] args) {
        var obj = Morse.getInstance();
        System.out.println(obj.encodeMessage("cool java"));
        obj.encodeAndPlayAudio("cool java");

    }
}

Die Audiodateien finden Sie in Wikipedia

  1. Punktton, der im Grunde ein 'E'ist, finden Sie hier !
  2. Dash Sound, der im Grunde ein 'T'ist, finden Sie hier !

Was zu überprüfen:

Ich möchte eine Überprüfung von Stil, Design und Funktion. Was wird gut gemacht, was sollte besser oder anders gemacht werden? Welche alternative Lösung würden Sie vorschlagen?

Bitte beachten Sie, dass dieses Projekt zu Spaß- und Bildungszwecken erstellt wurde und nicht Teil einer Universitätsaufgabe ist!.

Wie von @ Sᴀᴍ Onᴇᴌᴀ in den Kommentaren erläutert, werde ich meinen Code nicht aktualisieren, um Feedback von Antworten zu erhalten. "Dies widerspricht dem Frage + Antwort-Stil der Codeüberprüfung . " Hier ist also der aktuelle Status auf meinem Github .

Danke im Voraus :)

Antworten

3 RoToRa Aug 19 2020 at 22:02

Die getInstanceMethoden schränken die Klasse stark ein und sind eine Quelle potenzieller Fehler. Es gibt keinen Grund, warum es beispielsweise nicht möglich sein sollte, zwei Objekte mit Zugriff auf zwei verschiedene Datenbankdateien zu erstellen:

Morse morse1 = Morse.getInstance(Paths.get("file1"));
Morse morse2 = Morse.getInstance(Paths.get("file2"));

In diesem Beispiel wird jedoch morse2unerwartet nicht verwendet "file2", sondern es wird dieselbe Instanz morse1verwendet, die verwendet wird "file1".

(BEARBEITEN: Sie sollten Setter vermeiden, wenn Sie können. Unveränderliche Klassen sind normalerweise vorzuziehen. Wenn Sie beispielsweise Datenbanken zur Laufzeit ändern möchten, ist es vorzuziehen, ein neues Objekt mit dieser anderen Datenbank zu erstellen, als ein vorhandenes Objekt zu ändern.)


Die Konstruktoren sollten unterschiedlich strukturiert sein, damit die gesamte Logik / Validierung nur in einem einzigen Konstruktor stattfindet und die anderen Konstruktoren nur diesen einen Konstruktor mit den Standardwerten aufrufen.

BEARBEITEN: Derzeit haben Sie zwei Konstruktoren, die aufrufen checkForDataBase(), und einen anderen, der das Trennzeichen überprüft. Stattdessen sollten Sie (wahrscheinlich Morse(final Path dataBaseFile, final String separator, final Charset cs)) einen einzigen "Haupt" -Konstruktor haben, der die gesamte Validierung enthält, und die anderen sollten diesen mit den Standardwerten für die fehlenden Parameter aufrufen. Zum Beispiel:

private final static String DEFAULT_SEPARATOR = " ";
private final static CharSet DEFAULT_CHARSET = StandardCharsets.UTF_8;

public Morse(final Path dataBaseFile, final String separator, final Charset cs) {
   // All validation and setting instance fields here
}

public Morse() {
  this(defaultDatabaseFile());
  // or: this(defaultDatabaseFile(), DEFAULT_SEPARATOR, DEFAULT_CHARSET)
}

public Morse(final Path dataBaseFile) {
  this(dataBaseFile, DEFAULT_SEPARATOR);
  // or: this(dataBaseFile, DEFAULT_SEPARATOR, DEFAULT_CHARSET)
}

public Morse(final Path dataBaseFile, final String separator) {
  this(dataBaseFile, separator, DEFAULT_CHARSET);
}

Das Abrufen der Standarddatenbankdatei scheint etwas kompliziert zu sein, insbesondere mit dem fest codierten Klassendateinamen "Morse.class", der leicht übersehen werden kann, wenn die Klasse jemals umbenannt wird.

Wenn ich mich nicht irre (ich mag es nicht, mit Ressourcen umzugehen), sollte es möglich sein mit:

Paths.get(Morse.class.getResource("../Code.txt").toURI());

Das assertSchlüsselwort dient nicht zur Überprüfung von Parametern. Es wird während der Entwicklung verwendet, um Zustände abzufangen, die niemals auftreten sollten. assertwird normalerweise zur Produktionslaufzeit deaktiviert. Verwenden Sie stattdessen Objects.requireNonNull.


separator.contains(".")ist eine unzuverlässige Methode, um zu überprüfen, ob ein regulärer Ausdruck mit einem Punkt übereinstimmt, da es sich bei regulären Ausdrücken um Sonderzeichen handelt, die mit einem beliebigen Zeichen übereinstimmen. Es wäre wahrscheinlich besser, nach \.( "\\."als Java-Zeichenfolge) zu suchen. Oder lassen Sie den Benutzer nicht direkt einen regulären Ausdruck als Trennzeichen zuweisen, sondern stattdessen ein Array von Zeichen / Zeichenfolgen, aus denen Sie einen regulären Ausdruck erstellen.


Die Verwendung System.exit(1)in einer solchen Utility-Klasse ist unerwartet und daher eine schlechte Idee. Sie sollten hier eine Ausnahme auslösen, die Sie abfangen main()und möglicherweise dort verwenden System.exit()können.


checkForRegexValidityscheint unnötig komplex. Die ausgelöste Ausnahme muss nicht gespeichert werden. Einfach direkt zurück trueoder false:

private boolean checkForRegexValidity (String regex) {
    try {
        Pattern.compile(regex);
        return true;
    } catch (PatternSyntaxException exception) { 
        return false;
    }
}

Wenn beim Lesen der Datenbankdatei eine Ausnahme auftritt, drucken Sie nicht nur die Stapelverfolgung, sondern ignorieren Sie den Fehler. Persönlich würde ich die Ausnahme einfach durchgehen lassen und sie außerhalb dieser Klasse abfangen. Eigentlich könnte man einfach checkForDataBasedie IOException fallen lassen und einfach wegen der fehlenden Datei durchlaufen lassen.


Während Sie die Karte füllen, müssen Sie die Linien unnötig aufräumen und zweimal teilen. Mit einem zusätzlichen .mapSchritt im Stream, der vermieden werden kann:

return encodingSchema.stream().filter(s -> !s.equals(""))
      .map(e -> e.replaceAll(charSeparationRegex," ").strip().split("\\s+"))
      .filter(e -> e.length < 2) // also skip invalid lines
      .collect(Collectors.toMap(
                    e -> e[0]
                    ,  e -> e[1]
                    , (e1, e2) -> e2
                    , HashBiMap::create)
            );

Ich sehe keinen Sinn darin, ein BiMaphier zu verwenden. Wenn Sie ständig Einträge hinzufügen oder daraus entfernen, ist dies sicherlich eine gute Idee. In diesem Fall ist die Karte jedoch statisch, sodass ich nur zwei normale Karten erstellen würde.