ตัวเข้ารหัส / ตัวถอดรหัสรหัสมอร์สพร้อมความสามารถในการเล่น

Aug 19 2020

เมื่อเร็ว ๆ นี้ฉันต้องการสร้างตัวเข้ารหัส / ตัวถอดรหัสมอร์สที่มีความสามารถในการเล่นโปรแกรมจำเป็นต้องjava version >= 11ทำงาน

โปรแกรมต้องการสองสามjarวินาที:

  • com.google.common.collect.BiMap

  • javazoom.jl.player.Player

ฉันใช้BiMapเหตุผลดังต่อไปนี้:

bimap (หรือ "แผนที่สองทิศทาง") คือแผนที่ที่รักษาความเป็นเอกลักษณ์ของค่าต่างๆเช่นเดียวกับคีย์ต่างๆ ข้อ จำกัด นี้ทำให้ bimaps รองรับ "มุมมองผกผัน" ซึ่งเป็น bimap อื่นที่มีรายการเดียวกันกับ bimap นี้ แต่มีคีย์และค่าที่กลับด้าน อ้างอิง

ในฐานะนักแปลมอร์สออนไลน์จำนวนมากให้ใช้อักขระ'/'หรือ a ','เพื่อแปลเป็นช่องว่างที่ฉันใช้'\t'.

โครงสร้างที่ชาญฉลาดฉันใช้รูปแบบการออกแบบ Singleton เพื่อให้ผู้ใช้มีวัตถุจำนวน จำกัด ดังนั้นจึงไม่จำเป็นต้องสร้างวัตถุเพื่อเข้ารหัส / ถอดรหัสหากมีอยู่แล้ว

โปรแกรมมีคุณสมบัติดังต่อไปนี้:

  1. มีความยืดหยุ่นจึงสามารถอ่านจากฐานข้อมูลที่ต้องการได้

  2. เข้ากันได้กับทุกประเภทที่อนุญาตให้CharSetสนับสนุนโดย java (เมื่อใช้ชุดอักขระที่ถูกต้องเพื่ออ่านไฟล์บางไฟล์)

  3. การเล่นเสียงเพื่อช่วยให้ผู้คนเรียนรู้ที่จะเข้าใจรหัสมอร์สด้วยการได้ยิน!.

  4. ความสามารถในการเขียนผลลัพธ์ลงในไฟล์ตามเส้นทางที่ต้องการ

  5. โปรแกรมจะพิจารณา regex ในการอ่านไฟล์ฐานข้อมูลเนื่องจาก regex จะทำหน้าที่เป็นตัวคั่นระหว่างตัวอักษรจริงกับลำดับของจุดและขีดกลาง

ดังนั้นนี่คือรหัส:

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 +
                '}';
    }
}

ตัวอย่าง DataBasecode.txt (แน่นอนว่าสามารถขยายได้เมื่อต้องการ):

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   _____

ผู้ใช้หลักจะมีลักษณะดังนี้:

public class Main {

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

    }
}

ไฟล์เสียงสามารถพบได้ใน Wikipedia

  1. จุดเสียงซึ่งโดยพื้นฐานแล้ว'E'สามารถพบได้ที่นี่ !
  2. เสียงประซึ่งโดยทั่วไป'T'สามารถพบได้ที่นี่ !

สิ่งที่ต้องตรวจสอบ:

ฉันต้องการการตรวจสอบสไตล์การออกแบบและการใช้งาน อะไรคือสิ่งที่ดีสิ่งที่ควรทำดีกว่าหรือแตกต่างกันอย่างไร? คุณจะเสนอทางเลือกอื่นใด

โปรดทราบว่าโครงการนี้จัดทำขึ้นเพื่อความสนุกสนานและจุดประสงค์ทางการศึกษาและไม่ได้เป็นส่วนหนึ่งของงานมอบหมายของมหาวิทยาลัย!.

ตามที่อธิบายโดย @ SᴀᴍOnᴇᴌᴀในความคิดเห็นที่ฉันจะไม่อัปเดตรหัสของฉันกับความคิดเห็นของ incorporate จากคำตอบ "การทำเช่นนั้นไปกับสไตล์การตอบคำถามของ + รหัสตรวจสอบ" ดังนั้นนี่คือสถานะปัจจุบันของฉันGithub

ขอบคุณล่วงหน้า :)

คำตอบ

3 RoToRa Aug 19 2020 at 22:02

getInstanceวิธีการรุนแรง จำกัด ชั้นเรียนและเป็นที่มาของข้อผิดพลาดที่อาจเกิดขึ้น ไม่มีเหตุผลที่ไม่น่าจะเป็นไปได้ตัวอย่างเช่นการสร้างวัตถุสองชิ้นที่มีการเข้าถึงไฟล์ฐานข้อมูลที่แตกต่างกันสองไฟล์:

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

อย่างไรก็ตามในตัวอย่างนี้morse2โดยไม่คาดคิดไม่ได้ใช้"file2"แทนที่จะเป็นเช่นเดียวกับที่ใช้morse1"file1"

(แก้ไข: คุณควรหลีกเลี่ยง setters ถ้าทำได้โดยทั่วไปแล้วคลาสที่ไม่เปลี่ยนรูปจะเป็นที่ต้องการตัวอย่างเช่นหากคุณต้องการเปลี่ยนฐานข้อมูลในขณะรันไทม์คุณควรสร้างออบเจ็กต์ใหม่โดยใช้ฐานข้อมูลอื่นแทนการเปลี่ยนอ็อบเจ็กต์ที่มีอยู่)


ตัวสร้างควรมีโครงสร้างที่แตกต่างกันเพื่อให้ตรรกะ / การตรวจสอบความถูกต้องทั้งหมดเกิดขึ้นเพียงตัวเดียวและตัวสร้างอื่น ๆ จะเรียกตัวสร้างนั้นด้วยค่าเริ่มต้นเท่านั้น

แก้ไข: ขณะนี้คุณมีตัวสร้างสองตัวที่เรียกใช้checkForDataBase()และอีกตัวหนึ่งที่ตรวจสอบตัวคั่น แต่คุณควรมีคอนสตรัคเตอร์ "หลัก" ตัวเดียว (อาจMorse(final Path dataBaseFile, final String separator, final Charset cs)) มากกว่าที่จะมีการตรวจสอบความถูกต้องทั้งหมดและให้คนอื่นเรียกตัวนั้นโดยใช้ค่าเริ่มต้นสำหรับพารามิเตอร์ที่ขาดหายไป สำหรับ eaxmple:

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);
}

การเรียกไฟล์ฐานข้อมูลเริ่มต้นดูเหมือนจะซับซ้อนเล็กน้อยโดยเฉพาะอย่างยิ่งกับชื่อไฟล์คลาสที่เข้ารหัส"Morse.class"ซึ่งสามารถมองข้ามได้อย่างง่ายดายหากคลาสนั้นเคยเปลี่ยนชื่อ

เว้นแต่ฉันจะเข้าใจผิด (ฉันไม่ชอบจัดการทรัพยากร) ควรเป็นไปได้ด้วย:

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

assertคำหลักไม่ได้สำหรับการตรวจสอบพารามิเตอร์ ใช้ในระหว่างการพัฒนาเพื่อจับสถานะมากกว่าที่ไม่ควรเกิดขึ้น assertโดยปกติจะปิดใช้งานในรันไทม์การผลิต ใช้แทนObjects.requireNonNull.


separator.contains(".")เป็นวิธีที่ไม่น่าเชื่อถือในการตรวจสอบว่านิพจน์ทั่วไปตรงกับช่วงเวลาหรือไม่เนื่องจากเป็นอักขระพิเศษในนิพจน์ทั่วไปที่ตรงกับอักขระใด ๆ มันน่าจะดีกว่าถ้าตรวจสอบ\.( "\\."เป็นสตริง Java) หรืออาจไม่อนุญาตให้ผู้ใช้กำหนดนิพจน์ทั่วไปโดยตรงเป็นตัวคั่นเลย แต่เป็นอาร์เรย์ของอักขระ / สตริงแทนซึ่งคุณจะสร้างนิพจน์ทั่วไป


การใช้System.exit(1)ภายในคลาสยูทิลิตี้เช่นนี้เป็นสิ่งที่ไม่คาดคิดและเป็นความคิดที่ไม่ดี คุณควรทิ้งข้อยกเว้นไว้ที่นี่ซึ่งคุณสามารถจับได้main()และอาจใช้ที่System.exit()นั่นได้


checkForRegexValidityดูเหมือนซับซ้อนอย่างไม่น่าเชื่อ ไม่จำเป็นต้องจัดเก็บข้อยกเว้นที่ถูกโยนทิ้ง เพียงส่งคืนโดยตรงtrueหรือfalse:

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

เมื่อพบข้อยกเว้นเมื่ออ่านไฟล์ฐานข้อมูลอย่าเพิ่งพิมพ์การติดตามสแต็กและไม่ต้องสนใจข้อผิดพลาด โดยส่วนตัวแล้วฉันจะปล่อยให้ข้อยกเว้นผ่านไปและจับนอกชั้นเรียนนี้ จริงๆแล้วคุณสามารถวางcheckForDataBaseและมี IOException ได้เนื่องจากไฟล์ที่หายไป


ในระหว่างการเติมแผนที่คุณจะทำความสะอาดและแยกเส้นสองครั้งโดยไม่จำเป็น ด้วย.mapขั้นตอนเพิ่มเติมในสตรีมที่สามารถหลีกเลี่ยงได้:

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)
            );

ฉันไม่เห็นประเด็นในการใช้BiMapที่นี่จริงๆ หากคุณเพิ่มหรือลบรายการออกจากที่ใดอยู่ตลอดเวลาก็คงจะเป็นความคิดที่ดีอย่างแน่นอนอย่างไรก็ตามในกรณีนี้แผนที่เป็นแบบคงที่ดังนั้นฉันจะสร้างแผนที่ปกติสองแผนที่