ตัวเข้ารหัส / ตัวถอดรหัสรหัสมอร์สพร้อมความสามารถในการเล่น
เมื่อเร็ว ๆ นี้ฉันต้องการสร้างตัวเข้ารหัส / ตัวถอดรหัสมอร์สที่มีความสามารถในการเล่นโปรแกรมจำเป็นต้องjava version >= 11
ทำงาน
โปรแกรมต้องการสองสามjar
วินาที:
com.google.common.collect.BiMap
javazoom.jl.player.Player
ฉันใช้BiMap
เหตุผลดังต่อไปนี้:
bimap (หรือ "แผนที่สองทิศทาง") คือแผนที่ที่รักษาความเป็นเอกลักษณ์ของค่าต่างๆเช่นเดียวกับคีย์ต่างๆ ข้อ จำกัด นี้ทำให้ bimaps รองรับ "มุมมองผกผัน" ซึ่งเป็น bimap อื่นที่มีรายการเดียวกันกับ bimap นี้ แต่มีคีย์และค่าที่กลับด้าน อ้างอิง
ในฐานะนักแปลมอร์สออนไลน์จำนวนมากให้ใช้อักขระ'/'
หรือ a ','
เพื่อแปลเป็นช่องว่างที่ฉันใช้'\t'
.
โครงสร้างที่ชาญฉลาดฉันใช้รูปแบบการออกแบบ Singleton เพื่อให้ผู้ใช้มีวัตถุจำนวน จำกัด ดังนั้นจึงไม่จำเป็นต้องสร้างวัตถุเพื่อเข้ารหัส / ถอดรหัสหากมีอยู่แล้ว
โปรแกรมมีคุณสมบัติดังต่อไปนี้:
มีความยืดหยุ่นจึงสามารถอ่านจากฐานข้อมูลที่ต้องการได้
เข้ากันได้กับทุกประเภทที่อนุญาตให้
CharSet
สนับสนุนโดย java (เมื่อใช้ชุดอักขระที่ถูกต้องเพื่ออ่านไฟล์บางไฟล์)การเล่นเสียงเพื่อช่วยให้ผู้คนเรียนรู้ที่จะเข้าใจรหัสมอร์สด้วยการได้ยิน!.
ความสามารถในการเขียนผลลัพธ์ลงในไฟล์ตามเส้นทางที่ต้องการ
โปรแกรมจะพิจารณา 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
- จุดเสียงซึ่งโดยพื้นฐานแล้ว
'E'
สามารถพบได้ที่นี่ ! - เสียงประซึ่งโดยทั่วไป
'T'
สามารถพบได้ที่นี่ !
สิ่งที่ต้องตรวจสอบ:
ฉันต้องการการตรวจสอบสไตล์การออกแบบและการใช้งาน อะไรคือสิ่งที่ดีสิ่งที่ควรทำดีกว่าหรือแตกต่างกันอย่างไร? คุณจะเสนอทางเลือกอื่นใด
โปรดทราบว่าโครงการนี้จัดทำขึ้นเพื่อความสนุกสนานและจุดประสงค์ทางการศึกษาและไม่ได้เป็นส่วนหนึ่งของงานมอบหมายของมหาวิทยาลัย!.
ตามที่อธิบายโดย @ SᴀᴍOnᴇᴌᴀในความคิดเห็นที่ฉันจะไม่อัปเดตรหัสของฉันกับความคิดเห็นของ incorporate จากคำตอบ "การทำเช่นนั้นไปกับสไตล์การตอบคำถามของ + รหัสตรวจสอบ" ดังนั้นนี่คือสถานะปัจจุบันของฉันGithub
ขอบคุณล่วงหน้า :)
คำตอบ
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
ที่นี่จริงๆ หากคุณเพิ่มหรือลบรายการออกจากที่ใดอยู่ตลอดเวลาก็คงจะเป็นความคิดที่ดีอย่างแน่นอนอย่างไรก็ตามในกรณีนี้แผนที่เป็นแบบคงที่ดังนั้นฉันจะสร้างแผนที่ปกติสองแผนที่