재생 기능이있는 모스 부호 인코더 / 디코더

Aug 19 2020

최근에 재생 기능이있는 모스 인코더 / 디코더를 만들고 싶었는데 프로그램 java version >= 11을 실행 해야 합니다.

이 프로그램에는 몇 가지가 필요합니다 jar.

  • com.google.common.collect.BiMap

  • javazoom.jl.player.Player

나는를 사용하여 BiMap다음과 같은 이유로 :

바이 맵 (또는 "양방향 맵")은 해당 값과 키의 고유성을 유지하는 맵입니다. 이 제약 조건을 통해 Bimap은이 Bimap과 동일한 항목을 포함하지만 키와 값이 반전 된 또 다른 Bimap 인 "역보기"를 지원할 수 있습니다. 심판

많은 온라인 모스 번역가처럼 문자 '/'또는 a ','를 사용하여 공간으로 번역했습니다 '\t'.

구조적으로 저는 Singleton 디자인 패턴을 사용했습니다. 사용자가 제한된 양의 개체를 가질 수 있도록하기 위해 이미 존재하는 경우 인코딩 / 디코딩 할 개체를 만들 필요가 없습니다.

이 프로그램의 특징은 다음과 같습니다.

  1. 유연하므로 원하는 데이터베이스에서 읽을 수 있습니다.

  2. CharSetJava에서 지원하는 모든 종류의 호환 가능 (특정 파일을 읽기 위해 올바른 문자 집합을 사용할 때).

  3. 사람들이 듣고 모스 부호를 이해하는 법을 배울 수 있도록 오디오 재생!.

  4. 원하는 경로로 결과를 파일에 기록하는 기능.

  5. 이 프로그램은 정규식이 실제 문자와 점 및 대시 시퀀스 사이의 구분 기호 역할을하므로 데이터베이스 파일을 읽을 때 정규식을 고려합니다.

그래서 여기에 코드가 있습니다 :

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

데이터베이스 샘플code.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ᴇᴌᴀ에 의해 설명 된대로 "코드 검토의 질문 + 답변 스타일에 위배된다"는 답변의 피드백을 포함하도록 코드를 업데이트하지 않습니다. 따라서 여기에 내 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".

(편집 : 가능하면 setter를 피해야합니다. 일반적으로 불변 클래스가 선호됩니다. 예를 들어 런타임에 데이터베이스를 변경하려는 경우 기존 개체를 변경하는 것보다 다른 데이터베이스를 사용하여 새 개체를 만드는 것이 좋습니다.)


모든 논리 / 검증은 단일 생성자에서만 발생하고 다른 생성자는 기본값으로 해당 생성자 만 호출하도록 생성자를 다르게 구조화해야합니다.

편집 : 현재를 호출하는 두 개의 생성자 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정말 복잡해 보입니다. throw 된 예외를 저장할 필요가 없습니다. 직접 반환 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여기 에서 사용하는 요점을 잘 모르겠습니다 . 항목을 지속적으로 추가하거나 제거하는 경우 확실히 좋은 생각이지만이 경우 맵은 정적이므로 두 개의 노멀 맵을 생성합니다.