Привет, Habr! Я учусь на программиста в Питерском Политехе. Одно из моих заданий в курсовой работе было написание консольной утилиты. Решил поделиться своим небольшим опытом.
Для начала представлю вам саму формулировку задания, которое мне необходимо было выполнить:
Реализовать сжатие RLE (run-length encoding). Продумать алгоритм сжатия и формат файла, при котором сжатие «неудачных» данных не приводит к большому увеличению размера файла.
Command Line: pack-rle [-z|-u] [-out outputname.txt] inputname.txt
Упаковывает -z или распаковывает -u указанный в аргументе командной строки текстовый файл. Выходной файл указывается как -out outputname.txt, по умолчанию имя формируется из входного файла с добавлением расширения.
Кроме самой программы, следует написать автоматические тесты к ней.
Сам алгоритм:
Превращается в строку: 9W3B24W1B4W
Однако я чуть улучшил алгоритм, убрав добавление 1 перед одиночным символом, чтобы избежать ситуации, когда сжатая строка длиннее исходной. («TBTB» ->«1T1B1T1B» «TBTB»)
Для начала я решил написать основную логику программы. Начнем с упаковщика.
Файл PackRLE.kt
Распаковщик:
Теперь напишем main() с функцией, отвечающей за взаимодействие с файлами:
Теперь подробнее про парсер
Я использовал готовую библиотеку args4j с некоторыми изменениями под свои задачи.
Файл Parser.java
На этом в принципе всё. На тестах особо останавливаться не буду. Сделаны с помощью библиотеки junit. Единственное, что возможно стоит некоторого внимания, так это функция assertFileContent в файле PackRLETest.kt:
Я не считаю себя супер крутым кодером, поэтому с радостью прочту все ваши комментарии по поводу улучшения и оптимизации кода.
Готовый проект тут.
Для начала представлю вам саму формулировку задания, которое мне необходимо было выполнить:
Реализовать сжатие RLE (run-length encoding). Продумать алгоритм сжатия и формат файла, при котором сжатие «неудачных» данных не приводит к большому увеличению размера файла.
Command Line: pack-rle [-z|-u] [-out outputname.txt] inputname.txt
Упаковывает -z или распаковывает -u указанный в аргументе командной строки текстовый файл. Выходной файл указывается как -out outputname.txt, по умолчанию имя формируется из входного файла с добавлением расширения.
Кроме самой программы, следует написать автоматические тесты к ней.
Сам алгоритм:
Кодирование длин серий (англ. run-length encoding, RLE) или кодирование повторов — алгоритм сжатия данных, заменяющий повторяющиеся символы (серии) на один символ и число его повторов. Серией называется последовательность, состоящая из нескольких одинаковых символов. При кодировании (упаковке, сжатии) строка одинаковых символов, составляющих серию, заменяется строкой, содержащей сам повторяющийся символ и количество его повторов.Строка: WWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWBWWWW
Превращается в строку: 9W3B24W1B4W
Однако я чуть улучшил алгоритм, убрав добавление 1 перед одиночным символом, чтобы избежать ситуации, когда сжатая строка длиннее исходной. («TBTB» ->
Итак, приступим
Для начала я решил написать основную логику программы. Начнем с упаковщика.
Файл PackRLE.kt
//словарь для цифр, так как они у нас используются, как служебные символы
private val dictionary = mutableListOf('⌂', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'È','É')
fun encode(string: String): String {
if (string == "") return ""
//создаём StringBuilder
val result = StringBuilder()
//счётчик каждого отдельного символа
var count = 0
//предыдущий символ
var prev = string[0]
//перебираем символы
for (char in string) {
if (char != prev) {
//как только встретили другой символ, выполняем этот код
//если символов в строке больше одного
//добавляем счётчик.
if (count > 1) result.append(count)
//отдельная обработка цифр, так как они у нас используются как служебные
if (prev.isDigit()) result.append(dictionary.elementAt(prev - '0')) else result.append(prev)
count = 0
prev = char
}
count++
}
//повтор кода для обработки последнего символа
if (count > 1) result.append(count)
if (prev.isDigit()) result.append(dictionary.elementAt(prev - '0')) else result.append(prev)
return result.toString()
}
Распаковщик:
fun decode(str: String): String {
val result = StringBuilder()
var i = 0
while (i in str.indices) {
//подстрока содержащая длину последовательности
val times = str.substring(i).takeWhile { it.isDigit() }
//times.count() - даёт понять сколько значное число определяет длину
val count = times.count()
//прибавляем это значение к текущему индексу, чтобы получить индекс символа
//и ищем его в dictionary, чтобы определить цифру ли мы закодировали или другой символ
val index = dictionary.indexOf(str[i + count])
//если это цифра (index != -1), то вычисляем цифру
val char = if (index != -1) '0' + index else str[i + count]
//записываем символ в строку
//times.toIntOrNull() вернёт null, если в times записано не число, либо строка пустая
//?: 1 вместо null даст нам 1
repeat(times.toIntOrNull() ?: 1) { result.append(char) }
i += 1 + count
}
return result.toString()
}
Теперь напишем main() с функцией, отвечающей за взаимодействие с файлами:
fun main(args: Array<String>) {
Parser.main(args)
}
fun packRLE(pack: Boolean, inputFile: String, outputFile: String?) {
//разбиваем файл на строки
val inputStrings = File(inputFile).readLines()
//открываем выходной файл для записи
val outputStream = File(outputFile ?: inputFile).bufferedWriter()
outputStream.use {
for (string in inputStrings) {
//перебираем строки и отдаём каждую кодировать/декодировать
val newString = if (pack) encode(string) else decode(string)
//записываем строку в файл
it.write(newString)
it.newLine()
}
}
//выводим на консоль сообщение
println("Pack-rle: "+ if (pack) "pack" else {"unpack"}+" successful")
}
Теперь подробнее про парсер
Parser.main(args)
Я использовал готовую библиотеку args4j с некоторыми изменениями под свои задачи.
Файл Parser.java
public class Parser {
//опция распаковка, по умолчанию false, не может быть вызвана одновременно с -z
@Option(name = "-u", usage = "unpacking file", forbids = {"-z"})
private boolean unpack = false;
//опция упаковка, по умолчанию false, не может быть вызвана одновременно с -u
@Option(name = "-z", usage = "packing file", forbids = {"-u"})
private boolean pack = false;
//опция выходной файл
@Option(name = "-out", usage = "output to this file (default: inputname.txt)", metaVar = "OUTPUT")
private String out;
//остальные аргументы
@Argument
private List<String> arguments = new ArrayList<String>();
public static void main(String[] args) {
new Parser().parseArgs(args);
}
public void parseArgs(String[] args) {
CmdLineParser parser = new CmdLineParser(this);
try {
//пытаемся пропарсить
parser.parseArgument(args);
//если аргументы в строке некорректные
//выводим соответствующее сообщение об ошибке
if (arguments.isEmpty() || (!pack && !unpack) || (!arguments.get(0).equals("pack-rle") || arguments.size() != 2)) {
System.err.println("Error entering arguments (for correct input, see the example)");
System.err.println("pack-rle [options...] arguments...");
parser.printUsage(System.err);
System.err.println("\nExample: pack-rle [-u|-z] [-out outputname.txt] inputname.txt");
//кидаем исключение для того, чтобы мы могли сделать тесты
throw new IllegalArgumentException("");
}
} catch (CmdLineException e) {
//обработка исключения, предусмотренного библиотекой
System.err.println(e.getMessage());
System.err.println("pack-rle [options...] arguments...");
parser.printUsage(System.err);
System.err.println("\nExample: pack-rle [-u|-z] [-out outputname.txt] inputname.txt");
//аналогично
throw new IllegalArgumentException("");
}
//передаём имя входного файла
String input = arguments.get(1);
//передаём нашей основной функции парсированные агрументы
PackRLEKt.packRLE(pack, input, out);
}
}
На этом в принципе всё. На тестах особо останавливаться не буду. Сделаны с помощью библиотеки junit. Единственное, что возможно стоит некоторого внимания, так это функция assertFileContent в файле PackRLETest.kt:
private fun assertFileContent(expectedFile: String, actualFile: String): Boolean {
val expected = File(expectedFile).readLines()
val actual = File(actualFile).readLines()
for (i in actual.indices) {
if (expected[i] != actual[i]) return false
}
return expected.size == actual.size
}
Я не считаю себя супер крутым кодером, поэтому с радостью прочту все ваши комментарии по поводу улучшения и оптимизации кода.
Готовый проект тут.