FormatSpecification.java

package com.varnernet.gerb4j;

/**
 * Format specification for Gerber coordinate parsing.
 *
 * <p>Gerber files specify coordinate formats using FS commands like "FSLAX24Y24" which define
 * the number of integer and decimal places for X and Y coordinates.
 */
public class FormatSpecification {
    /**
     * Minimum leading digits for coordinate format.
     */
    private static final int MIN_LEADING_DIGITS = 1;
    /**
     * Maximum leading digits for coordinate format.
     */
    private static final int MAX_LEADING_DIGITS = 6;
    /**
     * Minimum trailing digits for coordinate format.
     */
    private static final int MIN_TRAILING_DIGITS = 5;
    /**
     * Maximum trailing digits for coordinate format.
     */
    private static final int MAX_TRAILING_DIGITS = 6;

    int xLeading;
    int xTrailing;
    int yLeading;
    int yTrailing;

    /**
     * Default no-arg to facilitate testing.
     */
    FormatSpecification() {
    }

    /**
     * Constructs a FormatSpecification from known format specs.
     *
     * @param xFmt The X coordinate format
     * @param yFmt The Y coordinate format
     */
    public FormatSpecification(final String xFmt, final String yFmt) {
        if (!xFmt.matches("[" + MIN_LEADING_DIGITS + "-" + MAX_LEADING_DIGITS + "][" + MIN_TRAILING_DIGITS + "-" + MAX_TRAILING_DIGITS + "]")) {
            throw new IllegalArgumentException("Invalid format specification for Gerber X coordinates.");
        }
        if (!yFmt.matches("[" + MIN_LEADING_DIGITS + "-" + MAX_LEADING_DIGITS + "][" + MIN_TRAILING_DIGITS + "-" + MAX_TRAILING_DIGITS + "]")) {
            throw new IllegalArgumentException("Invalid format specification for Gerber Y coordinates.");
        }
        setXFormat(xFmt.charAt(0), xFmt.charAt(1));
        setYFormat(yFmt.charAt(0), yFmt.charAt(1));
    }

    /**
     * Sets the X coordinate format.
     *
     * @param leading  the number of leading digits
     * @param trailing the number of trailing digits
     */
    void setXFormat(final char leading, final char trailing) {
        this.xLeading = Integer.parseInt("" + leading);
        this.xTrailing = Integer.parseInt("" + trailing);
    }

    /**
     * Sets the Y coordinate format.
     *
     * @param leading  the number of leading digits
     * @param trailing the number of trailing digits
     */
    void setYFormat(final char leading, final char trailing) {
        this.yLeading = Integer.parseInt("" + leading);
        this.yTrailing = Integer.parseInt("" + trailing);
    }

    /**
     * Parses an X coordinate string according to the format specification.
     *
     * @param val the coordinate string
     * @return the parsed double value
     */
    public double getX(final String val) {
        // Strip optional sign before checking digit count
        int digitLen = (val.startsWith("-") || val.startsWith("+")) ? val.length() - 1 : val.length();
        if (digitLen > xLeading + xTrailing) {
            throw new IllegalArgumentException(
                    "Coordinate Length cannot be longer than the configured scale and precision");
        }
        return Double.parseDouble(format(val, xLeading, xTrailing));
    }

    /**
     * Parses a Y coordinate string according to the format specification.
     *
     * @param val the coordinate string
     * @return the parsed double value
     */
    public double getY(final String val) {
        // Strip optional sign before checking digit count
        int digitLen = (val.startsWith("-") || val.startsWith("+")) ? val.length() - 1 : val.length();
        if (digitLen > yLeading + yTrailing) {
            throw new IllegalArgumentException(
                    "Coordinate Length cannot be longer than the configured scale and precision");
        }
        return Double.parseDouble(format(val, yLeading, yTrailing));
    }

    private String format(final String val, final int leading, final int trailing) {
        // Extract and preserve the sign so it is not included in digit padding
        boolean negative = val.startsWith("-");
        boolean positive = val.startsWith("+");
        String digits = (negative || positive) ? val.substring(1) : val;

        String padded = String.format("%" + (leading + trailing) + "s", digits).replace(' ', '0');
        String formatted = padded.substring(0, leading) + "." + padded.substring(leading);
        return negative ? "-" + formatted : formatted;
    }
}