Kotlin / Java console utility using args4j library

Hi Habr! I am studying to be a programmer at the St. Petersburg Polytechnic. One of my assignments in the term paper was writing a console utility. I decided to share my little experience.

To begin, I’ll introduce you the very formulation of the task that I needed to complete:

Implement RLE (run-length encoding) compression . To think over the compression algorithm and file format, in which the compression of "unsuccessful" data does not lead to a large increase in file size.

Command Line :
pack-rle [-z | -u] [-out outputname.txt] inputname.txt Wraps

-z or unpacks -u the text file specified in the command line argument. The output file is specified as -out outputname.txt, by default the name is formed from the input file with the addition of the extension.

In addition to the program itself, you should write automatic tests for it.


The algorithm itself:
Run-length encoding (RLE) encoding or repeating encoding is a data compression algorithm that replaces repeated characters (series) with one character and the number of repeats. A series is a sequence of several identical characters. When encoding (packing, compressing) a string of identical characters that make up a series is replaced by a string containing the repeating character itself and the number of its repeats.
Line: WWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWBWWWW
Turns into a line: 9W3B24W1B4W
However, I slightly improved the algorithm by removing the addition of 1 in front of a single character to avoid the situation when the compressed line is longer than the original. ("TBTB" -> "1T1B1T1B" "TBTB")

So let's get started


To begin with, I decided to write the main logic of the program. Let's start with the packer.

File 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()
}

Unpacker:

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

Now let's write main () with the function responsible for interacting with files:

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

Now more about the parser

Parser.main(args)

I used the ready-made args4j library with some changes for my tasks.

Parser.java file

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

That’s basically it. I will not dwell on the tests. Made using the junit library. The only thing that might be worth some attention, since it is a function assertFileContent file 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
    }

I do not consider myself a super cool coder, so I will gladly read all your comments regarding the improvement and optimization of the code.

The finished project is here.

Sources


  1. Wikipedia
  2. Args4j

All Articles