Converting Morse Code Challenge

Write a program which reads a file containing Morse Code and outputs the conversion.

Specifications


The first argument is a path to a file.   
The file contains multiple lines.
Each line is a string of morse code.


Each letter is separated by a space.
Each word is separated by two spaces.



Constraints


The input file is correctly formatted.
The Morse Code strings are alphanumeric.


Sample Input


.- ...- ..--- .-- .... .. . -.-. -..-  ....- .....  
-... .... ...--



Sample Output


  AV2WHIECX 45
  BH3


Source

My Solution:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class Solution {
    private static final Map&lt;String, Character&gt; morseAlphabet = new HashMap&lt;&gt;();

    static {
        morseAlphabet.put(".-", 'A');
        morseAlphabet.put("-...", 'B');
        morseAlphabet.put("-.-.", 'C');
        morseAlphabet.put("-..", 'D');
        morseAlphabet.put(".", 'E');
        morseAlphabet.put("..-.", 'F');
        morseAlphabet.put("--.", 'G');
        morseAlphabet.put("....", 'H');
        morseAlphabet.put("..", 'I');
        morseAlphabet.put(".---", 'J');
        morseAlphabet.put("-.-", 'K');
        morseAlphabet.put(".-..", 'L');
        morseAlphabet.put("--", 'M');
        morseAlphabet.put("-.", 'N');
        morseAlphabet.put("---", 'O');
        morseAlphabet.put(".--.", 'P');
        morseAlphabet.put("--.-", 'Q');
        morseAlphabet.put(".-.", 'R');
        morseAlphabet.put("...", 'S');
        morseAlphabet.put("-", 'T');
        morseAlphabet.put("..-", 'U');
        morseAlphabet.put("...-", 'V');
        morseAlphabet.put(".--", 'W');
        morseAlphabet.put("-..-", 'X');
        morseAlphabet.put("-.--", 'Y');
        morseAlphabet.put("--..", 'Z');
        morseAlphabet.put("-----", '0');
        morseAlphabet.put(".----", '1');
        morseAlphabet.put("..---", '2');
        morseAlphabet.put("...--", '3');
        morseAlphabet.put("....-", '4');
        morseAlphabet.put(".....", '5');
        morseAlphabet.put("-....", '6');
        morseAlphabet.put("--...", '7');
        morseAlphabet.put("---..", '8');
        morseAlphabet.put("----.", '9');
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println(args.length &gt; 1 ? "Excessive arguments, only the first will be considered" : "No arguments provided.");
            System.exit(1);
        }

        try (Scanner fileScanner = new Scanner(new File(args[0]))) {
            while (fileScanner.hasNextLine()) {
                System.out.println(decode(fileScanner.nextLine()));
            }
        } catch (FileNotFoundException fnfe) {
            fnfe.printStackTrace();
        }
    }

    private static String decode(String morse) {
        StringBuilder decoded = new StringBuilder();

        String[] words = morse.split("\\s{2}");
        for (String word : words) {
            decoded.append(' ');
            String[] letters = word.split("\\s");
            for (String letter : letters) {
                decoded.append(morseAlphabet.get(letter));
            }
        }

        return decoded.substring(1);
    }
}


Tests:

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class TestMorseConversion {
    @Test
    public void testCodeReviewMorse() {
        assertEquals(Morse.decode("-.-. --- -.. .  .-. . ...- .. . .--"), "CODE REVIEW");
    }

    @Test
    public void test2ndMonitor() {
        assertEquals(Morse.decode("..--- -. -..  -- --- -. .. - --- .-."), "2ND MONITOR");
    }

    @Test
    public void testFromCodeEval() {
        assertEquals(Morse.decode("..-. .-. --- --  -.-. --- -.. . . ...- .- .-.."), "FROM CODEEVAL");
    }
}


Note: The Morse class simply contains the map and decode method utilized above to facilitate testing.
Debatable ... and a bug?

constructs like these are one of those things that personal-preference plays in to, but consider:


    if (args.length != 1) {
        System.err.println(args.length &gt; 1 ? "Excessive arguments, only the first will be considered" : "No arguments provided.");
        System.exit(1);
    }



Error handling is boiler-plate that can be made more readable by separating the error conditions out explicitly. In checking/refactoring this, I found an apparent bug, too (which was hard to spot because of the compound exit condition). The bug is that if the arg-count is more than 1, you print the message "Excessive arguments, only the first will be considered" but then terminate anyway..... so much for ignoring the subsequent args...  I would prefer:

private static final void terminate(String message) {
    System.err.println(message);
    System.exit(1);
}

.....

    if (args.length == 0) {
        terminate("No arguments provided");
    }
    if (args.length &gt; 1) {
        terminate("Too many arguments provided. Expect exactly 1");
    }


A distinct advantage I find when you separate out the error conditions, is that maintaining them is simpler, especially when using version-control because the diffs are simpler to understand.

Challenge

Programming challenges are often time-constrained. While for most purposes your code is fast enough, there's a simple technique that can significantly improve performance - do bulk operations (at the expense of using memory). Specifically, it's typically better to "accumulate" the output in a "StringBuilder" and then have a single System.out.println() at the end. The in-the-loop println you currently have will cause the system to "flush" each line, and often that amounts to significant portions of your program time (more than half?).

Similarly, reading in the entire input file in one "gulp" is often faster than a Scanner, or some other operation.

Obviously, "huge" inputs may be a problem, in theory, but I have never yet been found victim to conditions like that in challenges.

Gulping

I would consider using a gulp function to read the entire file - the Java 8  java.nio.files package can help here, and I would choose:

List&lt;String&gt; data = Files.readAllLines(Paths.get(args[0]));


Splitting

A trick of loading an empty-string in to your decode-routine to output a space, will allow you to use a regex to split, and process each line, in a stream:

private static final Pattern charsplit = Pattern.compile(" ");


and then:

String out = charsplit.splitAsStream(line)
             .map(letter -&gt; morseAlphabet.get(letter))
             .collect(Collectors.joining(""));


You can then append that out with a newline, to the output buffer... so that the whole thing ends up looking a little like (untested - see conclusion for working code):

String output = Files.readAllLines(Paths.get(args[0])).stream()
     .map(line -&gt; charsplit.splitAsStream(line)
                           .map(letter -&gt; morseAlphabet.get(letter)
                           .collect(Collectors.joining("")))
     .collect(Collectors.joining("\n");

System.out.println(output);


I would probably take that inner stream and make a real Function&lt;String,String&gt; out of it. I would also make the printing a separate function ... it should not be part of the parsing/decoding.

Conclusion

I put together a working example program here: https://ideone.com/hCpUA1

Note that the version of the code I was happiest with separated out the various functions, and converted the Map&lt;String, Character&gt; to a Map&lt;String, String&gt;. Note that the map also has the "Space" value in it.

Additionally, the reading of the file, and converting to List&lt;String&gt; is expected to be outside the function (in the main method) which cannot be done in ideone.com.

Here's the code in full:

import java.util.*;
import java.lang.*;
import java.io.*;
import java.util.stream.*;
import java.util.regex.Pattern;


class Morse
{

    private static final Map&lt;String, String&gt; morseAlphabet = new HashMap&lt;&gt;();

    static {
        morseAlphabet.put("", " ");
        morseAlphabet.put(".-", "A");
        morseAlphabet.put("-...", "B");
        morseAlphabet.put("-.-.", "C");
        morseAlphabet.put("-..", "D");
        morseAlphabet.put(".", "E");
        morseAlphabet.put("..-.", "F");
        morseAlphabet.put("--.", "G");
        morseAlphabet.put("....", "H");
        morseAlphabet.put("..", "I");
        morseAlphabet.put(".---", "J");
        morseAlphabet.put("-.-", "K");
        morseAlphabet.put(".-..", "L");
        morseAlphabet.put("--", "M");
        morseAlphabet.put("-.", "N");
        morseAlphabet.put("---", "O");
        morseAlphabet.put(".--.", "P");
        morseAlphabet.put("--.-", "Q");
        morseAlphabet.put(".-.", "R");
        morseAlphabet.put("...", "S");
        morseAlphabet.put("-", "T");
        morseAlphabet.put("..-", "U");
        morseAlphabet.put("...-", "V");
        morseAlphabet.put(".--", "W");
        morseAlphabet.put("-..-", "X");
        morseAlphabet.put("-.--", "Y");
        morseAlphabet.put("--..", "Z");
        morseAlphabet.put("-----", "0");
        morseAlphabet.put(".----", "1");
        morseAlphabet.put("..---", "2");
        morseAlphabet.put("...--", "3");
        morseAlphabet.put("....-", "4");
        morseAlphabet.put(".....", "5");
        morseAlphabet.put("-....", "6");
        morseAlphabet.put("--...", "7");
        morseAlphabet.put("---..", "8");
        morseAlphabet.put("----.", "9");
    }


    private static final Pattern charsplit = Pattern.compile(" ");

    private static final String decodeLine(String line) {
        return charsplit.splitAsStream(line)
                   .map(letter -&gt; morseAlphabet.get(letter))
                   .collect(Collectors.joining(""));
    }

    public static final String decode(List&lt;String&gt; data) {
        String output = data.stream()
             .map(Morse::decodeLine)
             .collect(Collectors.joining("\n"));
        return output;
    }

    public static void main (String[] args) throws java.lang.Exception {
        String out = Morse.decode(Arrays.asList(
               "-.-. --- -.. .  .-. . ...- .. . .--",
               "..--- -. -..  -- --- -. .. - --- .-.",
               "..-. .-. --- --  -.-. --- -.. . . ...- .- .-.."));
        System.out.println(out);
    }

}