재생 기능이있는 모스 부호 인코더 / 디코더
최근에 재생 기능이있는 모스 인코더 / 디코더를 만들고 싶었는데 프로그램 java version >= 11
을 실행 해야 합니다.
이 프로그램에는 몇 가지가 필요합니다 jar
.
com.google.common.collect.BiMap
javazoom.jl.player.Player
나는를 사용하여 BiMap
다음과 같은 이유로 :
바이 맵 (또는 "양방향 맵")은 해당 값과 키의 고유성을 유지하는 맵입니다. 이 제약 조건을 통해 Bimap은이 Bimap과 동일한 항목을 포함하지만 키와 값이 반전 된 또 다른 Bimap 인 "역보기"를 지원할 수 있습니다. 심판
많은 온라인 모스 번역가처럼 문자 '/'
또는 a ','
를 사용하여 공간으로 번역했습니다 '\t'
.
구조적으로 저는 Singleton 디자인 패턴을 사용했습니다. 사용자가 제한된 양의 개체를 가질 수 있도록하기 위해 이미 존재하는 경우 인코딩 / 디코딩 할 개체를 만들 필요가 없습니다.
이 프로그램의 특징은 다음과 같습니다.
유연하므로 원하는 데이터베이스에서 읽을 수 있습니다.
CharSet
Java에서 지원하는 모든 종류의 호환 가능 (특정 파일을 읽기 위해 올바른 문자 집합을 사용할 때).사람들이 듣고 모스 부호를 이해하는 법을 배울 수 있도록 오디오 재생!.
원하는 경로로 결과를 파일에 기록하는 기능.
이 프로그램은 정규식이 실제 문자와 점 및 대시 시퀀스 사이의 구분 기호 역할을하므로 데이터베이스 파일을 읽을 때 정규식을 고려합니다.
그래서 여기에 코드가 있습니다 :
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에서 찾을 수 있습니다.
- 기본적으로있는 도트 사운드는 여기
'E'
에서 찾을 수 있습니다 ! - 기본적으로 대시 사운드는 여기
'T'
에서 찾을 수 있습니다 !
검토 할 내용 :
스타일, 디자인, 기능성 검토를 받고 싶습니다. 좋은 일이 무엇인지, 더 잘 또는 다르게해야 하는가? 어떤 대안을 제안 하시겠습니까?
이 프로젝트는 재미와 교육 목적으로 만들어졌으며 대학 과제의 일부가 아닙니다!.
주석에서 @ Sᴀᴍ Onᴇᴌᴀ에 의해 설명 된대로 "코드 검토의 질문 + 답변 스타일에 위배된다"는 답변의 피드백을 포함하도록 코드를 업데이트하지 않습니다. 따라서 여기에 내 Github 의 현재 상태가 있습니다.
미리 감사드립니다 :)
답변
이 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
여기 에서 사용하는 요점을 잘 모르겠습니다 . 항목을 지속적으로 추가하거나 제거하는 경우 확실히 좋은 생각이지만이 경우 맵은 정적이므로 두 개의 노멀 맵을 생성합니다.