4
\$\begingroup\$

Is there anything that I could have reworked here? I have a Java 7 mind but I'm working on getting my Java 8 ready, so if there are any improvements over readability of performance here I'd love to read

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
import java.util.regex.Pattern;

public final class EnglishNumberTranslator {

    private static final double MAX_LIMIT = 999999.99;
    private static final String OUT_OF_BOUNDS_INPUT = "Value must be greater than 0 "
                                                    + "and lower or equal to " + String.valueOf(MAX_LIMIT);
    private static final String INVALID_INPUT = "Unknow number pattern informed";
    private static final String ZERO = "zero dollars";

    private static final String[] oneToNineteenNames = {
        "", // sentinel value
        "one",
        "two",
        "three",
        "four",
        "five",
        "six",
        "seven",
        "eight",
        "nine",
        "ten",
        "eleven",
        "twelve",
        "thirteen",
        "fourteen",
        "fifteen",
        "sixteen",
        "seventeen",
        "eighteen",
        "nineteen"
    };

    private static final String[] tenToNinetyNames = {
        "", // sentinel value
        "ten",
        "twenty",
        "thirty",
        "forty",
        "fifty",
        "sixty",
        "seventy",
        "eighty",
        "ninety"
    };

    /**
     * Private constructor prevents initialization 
     * as this is only a method factory
     */
    private EnglishNumberTranslator() {/*deliberately empty*/}

    /**
     * Calls the chain of methods that translate the value to a string 
     * representation of the decimal values.
     * Dollars need to be separated by commas after three digits, such as: 
     * 12,345 for ten thousand three hundred and forty three,
     * and cents must be informed as two digits after a dot, such as
     * 1.45 for one and forty five 
     * @param value
     * @return String with the decimal translation of value
     * @throws IllegalArgumentException if value informed does not match the needed pattern
     */
    public static String convert(String value) throws IllegalArgumentException {
        double number;
        try {
            number = Double.parseDouble(getUSPatternNumber(value));
        } catch (ParseException e) {
            throw new IllegalArgumentException(e.getMessage());
        }

        if (number < 0.0 || number > (int) MAX_LIMIT) {
            throw new IllegalArgumentException(OUT_OF_BOUNDS_INPUT);
        }
        if (number == 0.0) { 
            return ZERO;
        }

        return capitalize(translate(value));
    }

    /**
     * enum type that represents either integer of floating point types
     */
    private enum NUM_TYPE {
        INTEGER,
        FLOATING_POINT
    }

    /**
     * Gets the words representation for numbers according to the decimal system
     * @param value to be translated to words
     * @return String decimal representation of the digits
     */
    private static String getHundredsTensOnes(int value) {
        StringBuilder returnValue = new StringBuilder();
        if (value % 100 < 20) {
            returnValue.append(oneToNineteenNames[value % 100]);
            value /= 100;
        } else {
            returnValue.append(oneToNineteenNames[value % 10]);
            value /= 10;
            returnValue.insert(0, tenToNinetyNames[value % 10] + " "); 
            value /= 10;
        }
        if (value > 0) {
            returnValue.insert(0, oneToNineteenNames[value] + " hundred ");
        }
        return returnValue.toString();
    }

    /**
     *  Translates the dollars value of a number
     * @param dollars value
     * @return words representation of dollars, empty string if dollars equal to zero
     */
    private static String translateDollars(String dollars) throws IllegalArgumentException, ParseException {
        StringBuilder dollarsStrBuilder = new StringBuilder(256);
        String[] values = dollars.split(",");
        int valuesIndex = 0;
        if (getIntValue(dollars) == 0) {
            // value is lower than one dollar, so print only cents
            return "";
        }   
        switch (values.length) {
            case 2: // thousands
                dollarsStrBuilder.append(getHundredsTensOnes(getIntValue(values[valuesIndex++])));
                dollarsStrBuilder.append(" thousand ");
            case 1: // hundreds or less
                dollarsStrBuilder.append(getHundredsTensOnes(getIntValue(values[valuesIndex])));
                break;
        }
        return dollarsStrBuilder.append(" dollar(s)").toString();
    }

    /**
     * Translates the cents values of a floating point value
     * @param cents value
     * @return words representation of cents, empty string if cents equal to zero
     */
    private static String translateCents(String cents) throws ParseException {
        if (cents.equals("00")) { 
            return "";
        }
        return getHundredsTensOnes(getIntValue(cents)) + " cent(s)";
    }

    /**
     * Does the translation of values from numeral to words
     * @param number to be translated
     * @return a String containing the word version of the informed number
     * @throws Exception
     */
    private static String translate(String number) throws IllegalArgumentException {
        StringBuilder numberInWords = new StringBuilder(256);
        try {
            switch (identifyType(number)) {
            case INTEGER:
                numberInWords.append(translateDollars(number));
                break;
            case FLOATING_POINT:
                int cents_position = number.length()-2;
                boolean isMoreThanOneDollar = true;
                String cents = "";

                numberInWords.append(translateDollars(number.substring(0, cents_position-1)));
                if (numberInWords.toString().equals("")) {
                    isMoreThanOneDollar = false;
                }
                cents = translateCents(number.substring(cents_position));
                if (isMoreThanOneDollar && !cents.equals("")) {
                    // gets the "and zero cents" case
                    numberInWords.append(" and ");
                }
                numberInWords.append(cents);
                break;
            default:
                throw new IllegalArgumentException(INVALID_INPUT);
            }
        } catch (IllegalArgumentException | ParseException ex) {
            System.err.println(ex);
            throw new IllegalArgumentException(ex);
        }
        return numberInWords.toString();
    }

    /**
     * Identifies if the informed number is an integer or a floating point number
     * @param number to be tested
     * @return {@code enum NUM_TYPE} value representing integer or floating point 
     */
    private static NUM_TYPE identifyType(String number) {
        if (isFloatingPoint(number)) {
            return NUM_TYPE.FLOATING_POINT;
        }
        return NUM_TYPE.INTEGER;
    }

    /**
     * @param number to be evaluated
     * @return true if value fits floating point pattern of string
     * @throws IllegalArgumentException if argument is of invalid type
     */
    private static boolean isFloatingPoint(String number) throws IllegalArgumentException {
        if (!isValid(number)) {
            throw new IllegalArgumentException(INVALID_INPUT);
        }
        return Pattern.matches(".*(\\.\\d\\d)$", number);
    }

    /**
     * Gets the digits of the given value by the US number standards, that is, 
     * 99,999 is transformed to 99999 
     * @param value to be evaluated
     * @return String containing the digits of the number
     * @throws ParseException if {@code value} is of unrecognizable value
     */
    private static String getUSPatternNumber(String value) throws ParseException {
        return NumberFormat.getNumberInstance(Locale.US).parse(value).toString();
    }

    /**
     * Capitalizes the first character of a string
     * @param line
     * @return String containing capitalized line
     */
    private static String capitalize(String line) {
       return Character.toUpperCase(line.charAt(0)) + line.substring(1);
    }

    /**
     * Helper function 
     * @param string to be evaluated
     * @return returns the integer representation of a string
     */
    private static int getIntValue(String s) throws ParseException {
        return Math.abs(Integer.valueOf(getUSPatternNumber(s)));
    }

    /**
     * Tests if user entered a number with error format regarding commas.
     * Commas required between powers of 1,000
     * Can't start with "."
     * Pass: (1,000,000), (0.01)
     * Fail: (1000000), (1,00,00,00), (.01)
     * @param values to be tested
     * @throws IllegalArgumentException if a single value has more than 3 digits 
     * @return true if value informed is within valid restrictions, false otherwise
     */
    private static boolean isValid(String number) {
         if (!Pattern.matches("^\\d{1,3}(,\\d{3})*(\\.\\d\\d)?$", number)) {
             return false;
         }
         return true;
    }
}

Examples:

public class Main {

    public static void main(String args[]) {
        try {
            System.out.println(EnglishNumberTranslator.convert("156,443.10"));
            System.out.println(EnglishNumberTranslator.convert("123"));
            System.out.println(EnglishNumberTranslator.convert("0.12"));
            System.out.println(EnglishNumberTranslator.convert("512.00"));          
        } catch (Exception e) {
            System.err.println(e);
        }
    }
}

Outputs the following:

One hundred fifty six thousand four hundred forty three dollar(s) and ten cent(s)
One hundred twenty three dollar(s)
Twelve cent(s)
Five hundred twelve dollar(s)
\$\endgroup\$
2
\$\begingroup\$

I like it that your methods have easy-to-understand Javadocs, keep it up! :)

A few of your helper methods have the following signature:

private static boolean method() {
    if (condition) {
        return true;
    }
    return false;
}

private static String anotherMethod() {
    if (condition) {
        return string;
    }
    return anotherString;
}

This is possible a choice of style, but you can also consider returning the condition result directly, or using a ternary operator for the non-boolean return types. Using isValid(String) for example:

private static boolean isValid(String number) {
     return Pattern.matches("^\\d{1,3}(,\\d{3})*(\\.\\d\\d)?$", number);
}

Also, technically speaking, this is not a simple numbers-to-english translation, but an English representation of monetary values. You can alternatively call your class as MonetaryValueTranslator.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.