SquirrelJME/SquirrelJME

View on GitHub
modules/cldc-compact/src/main/java/java/util/Formatter.java

Summary

Maintainability
A
1 hr
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package java.util;

import cc.squirreljme.runtime.cldc.annotation.Api;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Writer;
import org.intellij.lang.annotations.PrintFormat;

/**
 * This class implements C-style {@code printf()} formatted for text, it is
 * inspired by the C language but it does not match it exactly and this is
 * far more strict in what it requires.
 *
 * Format specifiers start with the {@code '%'} character which defines a
 * sequence accordingly. The format specifiers are in the following format:
 * {@code %[argument_index$][flags][width][.precision]conversion}. Any fields
 * within brackets are optional. For any specifiers which do not conform to
 * arguments, their format is {@code %[flags][width]conversion}.
 *
 * {@code argument_index$} specifies which argument to take the value from,
 * this allows one to use an alternative order. If this is not specified then
 * the order is implied based on the argument order for any specifiers which
 * do not use an argument index. This means that:
 * {@code format("%7$d %d %6$d %d %% %d", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)} will
 * print {@code "7 1 6 2 % 3"}.
 *
 * {@code flags} specifies special flags which are used to modify how the
 * printing is done. The following flags are used:
 *
 *  - {@code '-'} -- Left justified
 *    (general, char, int, float, date).
 *  - {@code '#'} -- Conversion dependent alternate form
 *    (general, int (only: o, x, X), float).
 *  - {@code '+'} -- Include a sign always
 *    (int, float).
 *  - {@code ' '} -- Include leading space for positive values
 *    (int, float).
 *  - {@code '0'} -- Zero padding
 *    (int, float).
 *  - {@code ','} -- Use locale specific grouping separators
 *    (int (only: d), float).
 *  - {@code '('} -- Negative values are enclosed in parenthesis
 *    (int, float).
 *
 * {@code width} is a positive decimal integer which specifies the minimum
 * amount of space the text will take up, it will be aligned accordingly
 * depending on the flags used.
 *
 * {@code precision} is a positive decimal integer which specified a limit
 * on the output, this depends on the format specifier.
 *
 * {@code conversion} is the symbol which determines how the input is to be
 * formatted.
 *
 * @since 2018/09/23
 */
@SuppressWarnings("OverlyComplexClass")
@Api
public final class Formatter
    implements Closeable
{
    /** The newline sequence. */
    private static final String _NEWLINE;
    
    /** The appendable to write to. */
    private final Appendable _out;
    
    /** Internal string buffer, used to output in entire chunks at once. */
    private final StringBuilder _buffer;
    
    /** Has this been closed? */
    private volatile boolean _closed;
    
    /** The IOException if any was generated. */
    private IOException _ioe;
    
    /**
     * Cache the line separator which is derived from the system properties.
     *
     * @since 2018/10/10
     */
    static
    {
        String nl;
        try
        {
            nl = System.getProperty("line.separator");
        }
        catch (SecurityException e)
        {
            nl = "\n";
        }
        
        _NEWLINE = nl;
    }
    
    /**
     * Initializes a formatter which uses an internal {@link StringBuilder}.
     *
     * @since 2018/09/23
     */
    @Api
    public Formatter()
    {
        this(new StringBuilder());
    }
    
    /**
     * Initializes the formatter writing to the given output.
     *
     * @param __a The output.
     * @throws NullPointerException On null arguments.
     * @since 2018/09/23
     */
    @Api
    public Formatter(Appendable __a)
        throws NullPointerException
    {
        if (__a == null)
            throw new NullPointerException("NARG");
        
        this._out = __a;
        
        // If the output is an actual StringBuilder then we do not need an
        // internal mini buffer for entirely chunked output.
        this._buffer = ((__a instanceof StringBuilder) ? null :
            new StringBuilder());
    }
    
    /**
     * Initializes the formatter writing to the given output.
     *
     * @param __ps The output.
     * @throws NullPointerException On null arguments.
     * @since 2018/09/23
     */
    @Api
    public Formatter(PrintStream __ps)
        throws NullPointerException
    {
        this((Appendable)__ps);
    }
    
    /**
     * {@inheritDoc}
     * @since 2018/09/23
     */
    @Override
    public void close()
    {
        // No effect?
        if (this._closed)
            return;
        
        // Set closed
        this._closed = true;
        
        // Can only be done if it is closeable
        Appendable out = this._out;
        if (out instanceof Closeable)
            try
            {
                ((Closeable)out).close();
            }
            catch (IOException e)
            {
                this._ioe = e;
            }
    }
    
    /**
     * Flushes the output formatter, if it is flushable.
     *
     * @throws IllegalStateException If this formatter was closed.
     * @since 2018/09/23
     */
    @Api
    public void flush()
        throws IllegalStateException
    {
        /* {@squirreljme.error ZZ2i The formatter has been closed.} */
        if (this._closed)
            throw new IllegalStateException("ZZ2i");
        
        // Java ME has no Flushable interface so only certain classes have
        // the flush method
        try
        {
            Appendable out = this._out;
            if (out instanceof OutputStream)
                ((OutputStream)out).flush();
            else if (out instanceof Writer)
                ((Writer)out).flush();
        }
        
        /* {@squirreljme.error ZZ2j Could not flush the formatter.} */
        catch (IOException e)
        {
            throw new RuntimeException("ZZ2j", e);
        }
    }
    
    /**
     * Prints formatted text to the output.
     *
     * @param __fmt The format specified.
     * @param __args The input arguments.
     * @return {@code this}.
     * @throws IllegalArgumentException If the format specifiers are not
     * valid.
     * @throws IllegalStateException If this formatter has been closed.
     * @throws NullPointerException If no format was specified.
     * @since 2018/09/23
     */
    @Api
    public Formatter format(@PrintFormat String __fmt, Object... __args)
        throws IllegalArgumentException, IllegalStateException,
            NullPointerException
    {
        if (__fmt == null)
            throw new NullPointerException("NARG");
        
        /* {@squirreljme.error ZZ2k This formatter has been closed.} */
        if (this._closed)
            throw new IllegalStateException("ZZ2k");
        
        // Force this to exist
        if (__args == null)
            __args = new Object[0];
        
        // Global state needed for argument handling.
        __PrintFGlobal__ pg = new __PrintFGlobal__(__args);
        
        // Writing to the appendable may cause an exception to occur
        try
        {
            StringBuilder buffer = this._buffer;
            Appendable finalOut = this._out;
            Appendable workingOut = (buffer != null ? buffer : finalOut);
            
            // Wipe the temporary buffer
            if (buffer != null)
                buffer.setLength(0);
            
            // Process input characters
            for (int i = 0, n = __fmt.length(), next = 0; i < n; i = next)
            {
                char c = __fmt.charAt(i);
                
                // Just a normal character
                if (c != '%')
                {
                    workingOut.append(c);
                    
                    // Just skip the single character
                    next = i + 1;
                    continue;
                }
                
                // It is simpler to handle the parsing of the specifier in
                // another method due to loops and variables
                __PrintFState__ pf = new __PrintFState__(pg);
                next = Formatter.__specifier(pf, i, __fmt);
                
                // Handle output of the specifier
                Formatter.__output(workingOut, pf);
            }
            
            // Output the entire buffer here all at once so that I/O operations
            // are reduced.
            if (buffer != null)
                finalOut.append(buffer);
        }
        
        // Catch any exception
        catch (IOException e)
        {
            this._ioe = e;
        }
        
        return this;
    }
    
    /**
     * Returns the last {@link IOException} that was thrown by the output.
     *
     * @return The last exception thrown or {@code null} if one was never
     * thrown.
     * @since 2018/09/23
     */
    @Api
    public IOException ioException()
    {
        return this._ioe;
    }
    
    /**
     * Returns the output.
     *
     * @return The output.
     * @throws IllegalStateException If this formatter was closed.
     * @since 2018/09/23
     */
    @Api
    public Appendable out()
        throws IllegalStateException
    {
        /* {@squirreljme.error ZZ2l The formatter has been closed.} */
        if (this._closed)
            throw new IllegalStateException("ZZ2l");
        
        return this._out;
    }
    
    /**
     * This performs {@code this.out().toString()} and returns that result, as
     * such the value here might not match what was written into the string.
     *
     * @return The string from the output.
     * @throws IllegalStateException If this formatter was closed.
     * @since 2018/09/23
     */
    @Override
    public String toString()
        throws IllegalStateException
    {
        /* {@squirreljme.error ZZ2m The formatter has been closed.} */
        if (this._closed)
            throw new IllegalStateException("ZZ2m");
        
        return this._out.toString();
    }
    
    /**
     * Formats integer number in decimal format.
     *
     * @param __pf The state used.
     * @param __n The number used.
     * @return The formatted integer number.
     * @throws NullPointerException If no state is specified.
     * @since 2018/11/03
     */
    private static String __formatDecimalInt(__PrintFState__ __pf, Number __n)
        throws NullPointerException
    {
        if (__pf == null)
            throw new NullPointerException("NARG");
        
        // Null is used.
        if (__n == null)
            return null;
        
        /* {@squirreljme.error ZZ3s Decimal integers cannot have an
        alternative form.} */
        if (__pf.__hasFlag(__PrintFFlag__.ALTERNATIVE_FORM))
            throw new IllegalArgumentException("ZZ3s");
        
        // Generate base number
        long value = __n.longValue();
        boolean neg = (value < 0);
        StringBuilder base = new StringBuilder(Long.toString(value));
        
        // Use local digit grouping
        if (__pf.__hasFlag(__PrintFFlag__.LOCALE_GROUPING))
            throw Debugging.todo();
        
        // Negative values can have flags (note negatives are always signed)
        boolean signed = neg;
        if (neg)
        {
            // Use parenthesis for negative instead
            if (__pf.__hasFlag(__PrintFFlag__.NEGATIVE_PARENTHESIS))
            {
                // Replace the sign with open parenthesis
                base.setCharAt(0, '(');
                
                // And add parenthesis to the end
                base.append(')');
            }
        }
        
        // Positive values have some flags
        else
        {
            // Always use the sign
            if (signed |= __pf.__hasFlag(__PrintFFlag__.ALWAYS_SIGNED))
                base.insert(0, '+');
            
            // Space for positive value
            else if (signed |= __pf.__hasFlag(
                __PrintFFlag__.SPACE_FOR_POSITIVE))
                base.insert(0, ' ');
        }
        
        // Padding with zero? Remember if there is a sign in place
        if (__pf.__hasFlag(__PrintFFlag__.ZERO_PADDED))
            for (int w = __pf.__width(), idx = (signed ? 1 : 0);
                base.length() < w;)
                base.insert(idx, '0');
        
        // Use this result
        return base.toString();
    }
    
    /**
     * Formats other unsigned value.
     *
     * @param __pf Printing state.
     * @param __n The number to print.
     * @param __base The number's base.
     * @throws 
     */
    private static String __formatOtherUnsignedInt(__PrintFState__ __pf,
        Number __n, int __base)
        throws NullPointerException
    {
        if (__pf == null)
            throw new NullPointerException("NARG");
        
        /* {@squirreljme.error ZZ3t Invalid flag specified for unsigned
        formatted value.} */
        if (__pf.__hasFlag(__PrintFFlag__.LOCALE_GROUPING) ||
            __pf.__hasFlag(__PrintFFlag__.NEGATIVE_PARENTHESIS) ||
            __pf.__hasFlag(__PrintFFlag__.ALWAYS_SIGNED) ||
            __pf.__hasFlag(__PrintFFlag__.SPACE_FOR_POSITIVE))
            throw new IllegalArgumentException("ZZ3t");
        
        // Get the long value of the given value, because it is limited to
        // the precision (byte:-2 becomes 0xFE)
        long input;
        if (__n instanceof Byte)
            input = __n.byteValue() & 0xFFL;
        else if (__n instanceof Short)
            input = __n.shortValue() & 0xFFFFL;
        else if (__n instanceof Integer)
            input = __n.intValue() & 0xFFFFFFFFL;
        else
            input = __n.longValue();
        
        // Bit shift and mask used for the value
        int shift, mask;
        switch (__pf._conv)
        {
            case OCTAL_INTEGER:
                mask = 0x7;
                shift = 3;
                break;
            
            default:
            case HEXADECIMAL_INTEGER:
                mask = 0xF;
                shift = 4;
                break;
        }
        
        // Build the output number, constantly shift it until it is zero
        StringBuilder out = new StringBuilder();
        for (long rest = input; rest != 0; rest >>>= shift)
        {
            // Mask out the value
            int val = (int)(rest & mask);
            
            // Add the character for this digit
            out.append(Character.forDigit(val, __base));
        }
        
        // Pad the remaining space with zero
        if (__pf.__hasFlag(__PrintFFlag__.ZERO_PADDED))
            for (int i = out.length(), n = __pf.__width(); i < n; i++)
                out.append('0');
        
        // Place in the alternative form somewhere
        if (__pf.__hasFlag(__PrintFFlag__.ALTERNATIVE_FORM))
        {
            // The length of the string
            int len = out.length();
            
            // Hex
            if (__base == 16)
            {
                // If the last two characters are zero, set the one before
                // last to X
                if (len >= 2 && out.charAt(len - 1) == '0' &&
                    out.charAt(len - 2) == '0')
                    out.setCharAt(len - 2, 'x');
                
                // If the last character is zero, then make it x and append
                // zero
                else if (len >= 1 && out.charAt(len - 1) == '0')
                {
                    out.setCharAt(len - 1, 'x');
                    out.append('0');
                }
                
                // Otherwise append 0x (reversed)
                else
                {
                    out.append('x');
                    out.append('0');
                }
            }
            
            // Octal
            else if (__base == 8)
            {
                // If the last character is not zero, then add zero to the end
                if (len < 1 && out.charAt(len - 1) != '0')
                    out.append('0');
            }
        }
        
        // Reverse string because it is in the other order
        out.reverse();
        
        // Use it
        return out.toString();
    }
    
    /**
     * Outputs to the state to the appendable.
     *
     * @param __out The output appendable.
     * @param __pf The standard state.
     * @throws IllegalArgumentException If the format is not valid.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2018/09/28
     */
    private static void __output(Appendable __out, __PrintFState__ __pf)
        throws IllegalArgumentException, IOException, NullPointerException
    {
        if (__out == null || __pf == null)
            throw new NullPointerException("NARG");
            
        __PrintFConversion__ conv = __pf._conv;
        __PrintFCategory__ cat = conv.__category();
        
        // Depends on the conversion
        String append;
        switch (conv)
        {
                // Newline sequence
            case NEWLINE:
                append = Formatter._NEWLINE;
                break;
                
                // Boolean
            case BOOLEAN:
                append = __pf.<Boolean>__argument(Boolean.class,
                    Boolean.FALSE).toString();
                break;
                
                // Single character
            case CHARACTER:
                Character cha = __pf.<Character>__argument(Character.class);
                append = (cha == null ? "null" : cha.toString());
                break;
                
                // Octal 
            case OCTAL_INTEGER:
                append = Formatter.__formatOtherUnsignedInt(__pf,
                    __pf.<Number>__argument(Number.class), 8);
                break;
            
                // Decimal Integer
            case DECIMAL_INTEGER:
                append = Formatter.__formatDecimalInt(__pf,
                    __pf.<Number>__argument(Number.class));
                break;
                
                // Hexadecimal Integer
            case HEXADECIMAL_INTEGER:
                append = Formatter.__formatOtherUnsignedInt(__pf,
                    __pf.<Number>__argument(Number.class), 16);
                break;
            
                // Simple string conversion
            case STRING:
                append = __pf.<Object>__argument(Object.class, "null").
                    toString();
                break;
                
                // Percent sign
            case PERCENT:
                append = "%";
                break;
            
                /* {@squirreljme.error ZZ2n Unimplemented conversion.
                (The conversion)} */
            default:
                throw Debugging.todo("ZZ2n " + conv);
        }
        
        // Convert to uppercase
        if (__pf._upper)
            append = append.toUpperCase();
        
        // General items may be reduced by the precision, at most
        if (cat == __PrintFCategory__.GENERAL)
        {
            int precision = __pf._precision;
            if (precision >= 0)
                append = append.substring(0,
                    Math.min(precision, append.length()));
        }
        
        // Width is either higher of the input width or the string length
        int strlen = append.length(),
            width = Math.max(strlen, __pf._width),
            padding = width - strlen;
        
        // Right justification spaces?
        boolean isleft = __pf.__isLeftJustified();
        if (!isleft)
            for (int i = 0; i < padding; i++)
                __out.append(' ');
        
        // Send the string in
        __out.append(append);
        
        // Left justification spaces?
        if (isleft)
            for (int i = 0; i < padding; i++)
                __out.append(' ');
    }
    
    /**
     * Parses the specifier in the input format.
     *
     * This parses the following, just returning that information:
     * {@code %[argument_index$][flags][width][.precision]conversion}.
     *
     * @param __pf The PrintF state.
     * @param __base The base character index.
     * @param __fmt The format specifiers.
     * @return The index where the format specifier ends, this is so the
     * calling loop can properly set its counter.
     * @throws IllegalArgumentException If the format specifiers are not
     * correct.
     * @throws NullPointerException On null arguments.
     * @since 2018/09/23
     */
    private static int __specifier(__PrintFState__ __pf, int __base,
        String __fmt)
        throws IllegalArgumentException, NullPointerException
    {
        if (__pf == null || __fmt == null)
            throw new NullPointerException("NARG");
        
        // Length of the format
        int fmtlen = __fmt.length();
        
        // The specifier could be too short! So handle these cases and make
        // sure the exception of the right type
        int at = __base + 1;
        try
        {
            char c = __fmt.charAt(at);
            
            // The first might be a number specifying the argument index to use
            // Note that zero is NOT included because it would be parsed as
            // a flag, additionally argument indexes are 1-based!
            if (c >= '1' && c <= '9')
            {
                // Read the entire number
                int base = at;
                for (;;)
                {
                    c = __fmt.charAt(at);
                    if (c >= '0' && c <= '9')
                        at++;
                    else
                        break;
                }
                
                // Parse value
                int val = Integer.valueOf(__fmt.substring(base, at));
                
                // If there is a dollar sign, this is going to be the
                // argument index
                if (__fmt.charAt(at) == '$')
                {
                    __pf.__setArgumentIndex(val);
                    
                    // Skip the dollar
                    at++;
                }
                
                // Otherwise this is the width
                else
                    __pf.__setWidth(val);
            }
            
            // Parse flags, but only if no width was specified
            if (!__pf.__hasWidth())
                for (c = __fmt.charAt(at); __pf.__setFlag(c);
                    at++, c = __fmt.charAt(at))
                    ;
            
            // Parse width
            c = __fmt.charAt(at);
            if (!__pf.__hasWidth() && c >= '1' && c <= '9')
            {
                // Read the entire number
                int base = at;
                for (;;)
                {
                    c = __fmt.charAt(at);
                    if (c >= '0' && c <= '9')
                        at++;
                    else
                        break;
                }
                
                // Set width
                __pf.__setWidth(Integer.valueOf(__fmt.substring(base, at)));
            }
            
            // Parse precision
            c = __fmt.charAt(at);
            if (c == '.')
                throw Debugging.todo();
            
            // Parse the conversion
            c = __fmt.charAt(at);
            if (c == 't' || c == 'T')
            {
                __pf.__setConversion(c, __fmt.charAt(at + 1));
                at++; 
            }
            else
                __pf.__setConversion(c, -1);
            
            // Skip conversion character
            at++;
        }
        
        /* {@squirreljme.error ZZ2o Could not parse the format specifier
        properly. (The string with the specifier sequence)} */
        catch (IndexOutOfBoundsException|NumberFormatException e)
        {
            throw new IllegalArgumentException("ZZ2o " +
                __fmt.substring(__base, Math.min(at, __fmt.length())), e);
        }
        
        // Return the new ending index, after everything was parsed
        return at;
    }
}