3
\$\begingroup\$

In perl, there is that format facility for console output. I thought it might be good to practice some library design instead of algorithms once in a while. This may be compared to a GUI library: you start from an empty window, add some lines to it, add labels and other stuff to each line and so on. At this point I have this:

FormatWindow.java:

package net.coderodde.textui;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class defines a formatted text window.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public class FormatWindow {

    private final List<FormatWindowLine> formattedLineList = new ArrayList<>();
    private final Map<String, String> fieldNameToValueMap  = new HashMap<>();

    /**
     * Adds another line to this formatter.
     * 
     * @param formatWindowLine a new line descriptor.
     */
    public void addFormatWindowLine(final FormatWindowLine formatWindowLine) {
        if (formatWindowLine != null) {
            formattedLineList.add(formatWindowLine);
        }
    }

    public void setFieldValue(final String fieldName, final String value) {
        if (fieldName != null) {
            fieldNameToValueMap.put(fieldName, value);
        }
    }

    @Override 
    public String toString() {
        if (formattedLineList.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder()
                .append(formattedLineList.get(0).toText(fieldNameToValueMap));

        for (int i = 1; i < formattedLineList.size(); ++i) {
            sb.append("\n")
              .append(formattedLineList.get(i).toText(fieldNameToValueMap));
        }

        return sb.toString();
    }
}

FormatWindowLine.java:

package net.coderodde.textui;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class implements a descriptor for formatted text output on a single
 * line.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public class FormatWindowLine {

    private final List<AbstractFormatWindowField> formatWindowFieldList = 
            new ArrayList<>();

    public FormatWindowLine addFormatWindowField(
            final AbstractFormatWindowField formatWindowField) {
        if (formatWindowField != null) {
            formatWindowFieldList.add(formatWindowField);
        }

        return this;
    }

    public FormatWindowLine addFormatWindowLine(FormatWindowLine other) {
        if (other != null) {
            other.formatWindowFieldList
                 .forEach((field) -> { addFormatWindowField(field); });
        }

        return this;
    }

    String toText(final Map<String, String> mapFieldNameToValue) {
        StringBuilder sb = new StringBuilder();

        for (AbstractFormatWindowField field : formatWindowFieldList) {
            sb.append(field.toText(mapFieldNameToValue.get(field.getName())));
        }

        return sb.toString();
    }
}

FormatWindowFieldAlignment.java:

package net.coderodde.textui;

/**
 * This enumeration specifies all possible ways of aligning a text token within
 * a {@link net.coderodde.textui.FormatWindowField}.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public enum FormatWindowFieldAlignment {
    LEFT,
    CENTER,
    RIGHT,
}

AbstractFormatWindowField.java:

package net.coderodde.textui;

/**
 * This class defines a text field. A field is specified by its offset from the
 * start of the line, its maximum allowed length and its alignment.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public abstract class AbstractFormatWindowField {

    public abstract String toText(String value);

    public abstract String getName();

    protected void checkNotNegative(int number, String errorMessage) {
        if (number < 0) {
            throw new IllegalArgumentException(errorMessage + ": " + number);
        }
    }

    protected void checkPositive(int number, String errorMessage) {
        if (number <= 0) {
            throw new IllegalArgumentException(errorMessage);
        }
    }

    protected void checkMaximumLength(int maximumLength) {
        checkNotNegative(maximumLength, "The maximum field length is negative");

        if (maximumLength == 0) {
            throw new IllegalArgumentException(
                    "The maximum field length is zero.");
        }
    }
}

BarField.java:

package net.coderodde.textui.support;

import net.coderodde.textui.AbstractFormatWindowField;

/**
 * This class implements a fixed-length character bar. A bar is a sequence of 
 * only one character.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public class BarField extends AbstractFormatWindowField {

    private final char barCharacter;
    private final int length;

    public BarField(final char barCharacter, final int length) {
        checkPositive(length, "The bar length must be a positive number");
        this.barCharacter = barCharacter;
        this.length = length;
    }

    @Override
    public String toText(String value) {
        StringBuilder sb = new StringBuilder(length);

        for (int i = 0; i < length; ++i) {
            sb.append(barCharacter);
        }

        return sb.toString();
    }

    @Override
    public String getName() {
        return null;
    }
}

StringField.java:

package net.coderodde.textui.support;

import java.util.Objects;
import net.coderodde.textui.AbstractFormatWindowField;

/**
 * This class implements a simple field consisting of a string.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public class StringField extends AbstractFormatWindowField {

    private final String string;

    public StringField(final String string) {
        this.string = Objects.requireNonNull(string, 
                                             "The input string is null.");
    }

    @Override
    public String toText(String value) {
        return string;
    }

    @Override
    public String getName() {
        return null;
    }
}

TextField.java:

package net.coderodde.textui.support;

import java.util.Objects;
import net.coderodde.textui.AbstractFormatWindowField;
import net.coderodde.textui.FormatWindowFieldAlignment;

/**
 * This class defines a simple text field.
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 13, 2016)
 */
public class TextField extends AbstractFormatWindowField {

    private static final FormatWindowFieldAlignment DEFAULT_ALIGNMENT = 
                         FormatWindowFieldAlignment.CENTER;
    private static final char DEFAULT_PAD_CHARACTER = ' ';

    private final String name;
    private final char padCharacter;
    private final FormatWindowFieldAlignment alignment;
    private final int maximumLength;

    public TextField(String name,
                     char padCharacter,
                     FormatWindowFieldAlignment alignment,
                     int maximumLength) {
        this.name = Objects.requireNonNull(
                name, 
                "The name of the field may not be null.");

        checkMaximumLength(maximumLength);
        this.padCharacter = padCharacter;
        this.alignment = alignment == null ? DEFAULT_ALIGNMENT : alignment;
        this.maximumLength = maximumLength;
    }

    public TextField(String name,
                     FormatWindowFieldAlignment alignment,
                     int maximumLength) {
        this(name, DEFAULT_PAD_CHARACTER, alignment, maximumLength);
    }

    public TextField(String name, int maximumLength) {
        this(name, FormatWindowFieldAlignment.CENTER, maximumLength);
    }

    public String getName() {
        return name;
    }

    @Override
    public String toText(String value) {
        value = value == null ? "" : value;

        if (value.length() > maximumLength) {
            return value.substring(0, maximumLength);
        }

        switch (alignment) {
            case LEFT:
                return toTextOnLeft(value);

            case CENTER:
                return toTextAtCenter(value);

            case RIGHT:
                return toTextOnRight(value);

            default:
                throw new IllegalStateException("Unknown enumeration.");
        }
    }

    private String toTextOnLeft(String value) {
        StringBuilder sb = new StringBuilder(maximumLength);
        sb.append(value);

        while (sb.length() < maximumLength) {
            sb.append(padCharacter);
        }

        return sb.toString();
    }

    private String toTextAtCenter(String value) {
        StringBuilder sb = new StringBuilder(maximumLength);
        int leftPaddingLength  = (maximumLength - value.length()) / 2;
        int rightPaddingLength = maximumLength - leftPaddingLength 
                                               - value.length();

        for (int i = 0; i < leftPaddingLength; ++i) {
            sb.append(padCharacter);
        }

        sb.append(value);

        for (int i = 0; i < rightPaddingLength; ++i) {
            sb.append(padCharacter);
        }

        return sb.toString();
    }

    private String toTextOnRight(String value) {
        StringBuilder sb = new StringBuilder(maximumLength);
        int leftPaddingLength = maximumLength - value.length();

        for (int i = 0; i < leftPaddingLength; ++i) {
            sb.append(padCharacter);
        }

        return sb.append(value).toString();
    }
}

Demo.java:

import java.util.Arrays;
import net.coderodde.textui.support.BarField;
import net.coderodde.textui.FormatWindow;
import net.coderodde.textui.FormatWindowFieldAlignment;
import net.coderodde.textui.FormatWindowLine;
import net.coderodde.textui.support.StringField;
import net.coderodde.textui.support.TextField;

public class Demo {

    public static void main(String[] args) {
        FormatWindow window = new FormatWindow();

        FormatWindowLine[] lines = new FormatWindowLine[]{
            new FormatWindowLine(),
            new FormatWindowLine(),
            new FormatWindowLine(),
            new FormatWindowLine(),
            new FormatWindowLine(),
            new FormatWindowLine()
        };

        BarField horizontalLine = new BarField('*', 80);
        BarField innerLine      = new BarField(' ', 78);

        lines[0].addFormatWindowField(horizontalLine);
        lines[1].addFormatWindowField(new StringField("*"))
                .addFormatWindowField(innerLine)
                .addFormatWindowField(new StringField("*"));

        lines[2].addFormatWindowField(new StringField("*  First name:  "))
                .addFormatWindowField(
                        new TextField("firstName", 
                                      '_', 
                                      FormatWindowFieldAlignment.CENTER, 
                                      20))
                .addFormatWindowField(new BarField(' ', 43))
                .addFormatWindowField(new StringField("*"));



        lines[3].addFormatWindowField(new StringField("*  Last name:   "))
                .addFormatWindowField(
                        new TextField("lastName",
                                      '_',
                                      FormatWindowFieldAlignment.RIGHT,
                                      20))
                .addFormatWindowField(new BarField(' ', 43))
                .addFormatWindowField(new StringField("*"));

        lines[4].addFormatWindowLine(lines[1]);
        lines[5].addFormatWindowLine(lines[0]);

        Arrays.stream(lines).forEach(window::addFormatWindowLine);

        // Manipulating window content is easy:
        window.setFieldValue("firstName", "Ken");
        window.setFieldValue("lastName", "Thompson");

        System.out.println(window);
    }
}

The output is


********************************************************************************
*                                                                              *
*  First name:  ________Ken_________                                           *
*  Last name:   ____________Thompson                                           *
*                                                                              *
********************************************************************************

Any critique is much appreciated!

\$\endgroup\$
4
\$\begingroup\$

Code feedback

No validation performed on field names

There is no validation performed on field names, if you forgot to include a field or misspell its name, it will show up as "null" on the final grid.

Should use codepoints instead of characters

You are assuming that a character in Java represents 1 characters, this is not true because there are more characters than a character can hold, and they are represented by multiple characters.

Instead of getting the length of a string by using String.length(), use String.codePointCount(0, String.length())

An extreme example of this where 1 character is more characters than 1 Java char is this: n̙̹͔͎̳̰̤̆͂̾ͩ̐̏. Java counts that using String.length as 14 characters, while we see it as 1 codepoint or 1 character. Constructs similar to this are commonly used with exotic human languages, and are usually the first thing to break when translating the program to those languages.

Don't fail silently

public FormatWindowLine addFormatWindowField(
        final AbstractFormatWindowField formatWindowField) {
    if (formatWindowField != null) {
        formatWindowFieldList.add(formatWindowField);
    }

    return this;
}

public FormatWindowLine addFormatWindowLine(FormatWindowLine other) {
    if (other != null) {
        other.formatWindowFieldList
             .forEach((field) -> { addFormatWindowField(field); });
    }

    return this;
}

public void addFormatWindowLine(final FormatWindowLine formatWindowLine) {
    if (formatWindowLine != null) {
        formattedLineList.add(formatWindowLine);
    }
}

public void setFieldValue(final String fieldName, final String value) {
    if (fieldName != null) {
        fieldNameToValueMap.put(fieldName, value);
    }
}

By silently failing, you are hiding the fact that there was a problem, leaving the user wondering why the code forgot to output a certain line, or forgot to set a field.

Don't make a string is its directly going to be used in a stringbuilder again

   for (int i = 1; i < formattedLineList.size(); ++i) {
        sb.append("\n")
          .append(formattedLineList.get(i).toText(fieldNameToValueMap));
    }
....
String toText(final Map<String, String> mapFieldNameToValue) {
    StringBuilder sb = new StringBuilder();

    for (AbstractFormatWindowField field : formatWindowFieldList) {
        sb.append(field.toText(mapFieldNameToValue.get(field.getName())));
    }

    return sb.toString();
}

You effectively making a string, just to be used once and thrown away. By making the toText() accept a stringbuilder instead, you prevent a useless stringbuilder and string from being made per-field, leaving less stress on the garbage collector.


Design feedback

Your output format is hardcoded

Image your business requirements change to use a 60 character output format instead of 80, you are going to need to change a lot of lines to make it look correctly.

Should use builder pattern

By using the builder pattern, you can get a more cleaner design. An example how can you can create a GUI using a library:

new FormatWindowBuilder ()
    .box("*")
        .box(" ")
            .addEmptyLine()
            .startLine()
                .addLabel("First name:  ")
                .addField("firstname", 
                          '_', 
                          FormatWindowFieldAlignment.CENTER, 
                          20)
            .endLine()
            .startLine()
                .addLabel("Last name:   ")
                .addField("lastname", 
                          '_', 
                          FormatWindowFieldAlignment.RIGHT, 
                          20)
            .endLine()
            .addEmptyLine()
        .endBox()
    .endBox()
.build()
.renderAsString(80, 80);

Code remarks

There are also things you did better than most people, and don't require changes that directly.

    StringBuilder sb = new StringBuilder(length);

    for (int i = 0; i < length; ++i) {
        sb.append(barCharacter);
    }

    return sb.toString();

Nice usage of the length argument of stringbuilder to prevent use character[] arrays from being made on the background. While this could be sped up by creating a character[] array directly and using Arrays.fill(), this is the most clear for everyone.

\$\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.