public StringBuffer format()

in endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/AngleFormat.java [1043:1210]


    public StringBuffer format(final double angle, StringBuffer toAppendTo, final FieldPosition pos) {
        final int offset = toAppendTo.length();
        final int fieldPos = getField(pos);
        if (!Double.isFinite(angle)) {
            toAppendTo = numberFormat().format(angle, toAppendTo, dummyFieldPosition());
            if (fieldPos >= DEGREES_FIELD && fieldPos <= SECONDS_FIELD) {
                pos.setBeginIndex(offset);
                pos.setEndIndex(toAppendTo.length());
            }
            return toAppendTo;
        }
        /*
         * Computes the numerical values of minutes and seconds fields.
         * If those fiels are not written, then store NaN.
         */
        double degrees = angle;
        double minutes = NaN;
        double seconds = NaN;
        int maximumFractionDigits = fractionFieldWidth;
        if (minutesFieldWidth == 0) {
            final double p = pow10(maximumFractionDigits);
            degrees = round(angle, degrees * p) / p;
        } else {
            minutes = Math.abs(degrees - (degrees = truncate(degrees))) * 60;
            /*
             * Limit the maximal number of fraction digits to the number of significant digits for a `double` value.
             * The intent is to avoid non-significant garbage that are pure artifacts from the conversion from base
             * 2 to base 10.
             */
            final int n = fractionDigitsForDelta(Math.ulp(angle) * (secondsFieldWidth == 0 ? 60 : 3600), false);
            maximumFractionDigits = Math.max(minimumFractionDigits,
                                    Math.min(maximumFractionDigits, n - 1));
            final double p = pow10(maximumFractionDigits);
            if (secondsFieldWidth != 0) {
                seconds = (minutes - (minutes = truncate(minutes))) * 60;
                seconds = round(angle, seconds * p) / p;                     // Correction for rounding errors.
                if (seconds >= 60) {                    // We do not expect > 60 (only == 60), but let be safe.
                    seconds = 0;
                    minutes++;
                }
            } else {
                minutes = round(angle, minutes * p) / p;                     // Correction for rounding errors.
            }
            if (minutes >= 60) {                        // We do not expect > 60 (only == 60), but let be safe.
                minutes = 0;
                degrees += Math.signum(angle);
            }
            /*
             * Note: a previous version was doing a unconditional addition to the 'degrees' variable,
             * in the form 'degrees += correction'. However, -0.0 + 0 == +0.0, while we really need to
             * preserve the sign of negative zero. See [SIS-120].
             */
        }
        /*
         * Avoid formatting values like 12.01°N as 12°36″N because of the risk of confusion.
         * In such cases, force the formatting of minutes field as in 12°00′36″.
         */
        byte effectiveOptionalFields = optionalFields;
        if (showLeadingFields) {
            effectiveOptionalFields &= ~(1 << DEGREES_FIELD);
            if (minutes == 0 && ((effectiveOptionalFields & (1 << SECONDS_FIELD)) == 0 || seconds != 0)) {
                effectiveOptionalFields &= ~(1 << MINUTES_FIELD);
            }
        }
        /*
         * At this point the 'degrees', 'minutes' and 'seconds' variables contain the final values to format.
         * The following loop will format fields from DEGREES_FIELD to SECONDS_FIELD inclusive.
         * The NumberFormat will be reconfigured at each iteration.
         */
        int field = PREFIX_FIELD;
        if (prefix != null) {
            toAppendTo.append(prefix);
        }
        final NumberFormat numberFormat = numberFormat();
        boolean hasMore;
        do {
            int    width;
            double value;
            String suffix;
            switch (++field) {
                case DEGREES_FIELD: value=degrees; width=degreesFieldWidth; suffix=degreesSuffix; hasMore=(minutesFieldWidth != 0); break;
                case MINUTES_FIELD: value=minutes; width=minutesFieldWidth; suffix=minutesSuffix; hasMore=(secondsFieldWidth != 0); break;
                case SECONDS_FIELD: value=seconds; width=secondsFieldWidth; suffix=secondsSuffix; hasMore=false; break;
                default: throw new AssertionError(field);
            }
            /*
             * If the value is zero and the field is optional, propagate the sign to the next field
             * and skip the whole field. Otherwise process to the formatting of current field.
             */
            if (value == 0 && (effectiveOptionalFields & (1 << field)) != 0) {
                switch (field) {
                    case DEGREES_FIELD: minutes = Math.copySign(minutes, degrees); break;
                    case MINUTES_FIELD: seconds = Math.copySign(seconds, minutes); break;
                }
                continue;
            }
            /*
             * Configure the NumberFormat for the number of digits to write, but do not write anything yet.
             */
            if (hasMore) {
                numberFormat.setMinimumIntegerDigits(width);
                numberFormat.setMaximumFractionDigits(0);
            } else if (useDecimalSeparator) {
                numberFormat.setMinimumIntegerDigits(width);
                if (maximumTotalWidth != 0) {
                    /*
                     * If we are required to fit the formatted angle in some maximal total width
                     * (i.e. the user called the setMaximumWidth(int) method), compute the space
                     * available for fraction digits after we removed the space for the integer
                     * digits, the decimal separator (this is the +1 below) and the suffix.
                     */
                    int available = maximumTotalWidth - toAppendTo.codePointCount(offset, toAppendTo.length());
                    available -= (width + 1);       // Remove the number of code points that we plan to write.
                    if (suffix != null) {
                        width -= suffix.length();
                    }
                    for (double scale=pow10(width); value >= scale; scale *= 10) {
                        if (--available <= 0) break;
                    }
                    if (available < maximumFractionDigits) {
                        maximumFractionDigits = Math.max(available, 0);
                    }
                }
                numberFormat.setMinimumFractionDigits(minimumFractionDigits);
                numberFormat.setMaximumFractionDigits(maximumFractionDigits);
            } else {
                value *= pow10(fractionFieldWidth);
                numberFormat.setMaximumFractionDigits(0);
                numberFormat.setMinimumIntegerDigits(width + fractionFieldWidth);
            }
            /*
             * At this point, we known the value to format and the NumberFormat instance has been
             * configured. If the user asked for an attributed character iterator and assuming that
             * we want also the attributes produced by the NumberFormat, then we have to invoke the
             * heavy formatToCharacterIterator(…). Otherwise the usual format(…) method fits well.
             */
            final int startPosition = toAppendTo.length();
            if (characterIterator != null) {
                final FormattedCharacterIterator it = characterIterator;
                it.append(numberFormat.formatToCharacterIterator(value), toAppendTo);
                if (suffix != null) {
                    toAppendTo.append(suffix);
                }
                final Number userObject;
                if (hasMore) {
                    userObject = Math.toIntExact(Math.round(value));
                } else {
                    /*
                     * Use Float instead of Double because we don't want to give a false impression of accuracy
                     * (when formatting the seconds field, at least the 10 last bits of the 'double' value are
                     * non-significant).
                     */
                    userObject = (float) value;
                }
                it.addFieldLimit(Field.forCode(field), userObject, startPosition);
            } else {
                toAppendTo = numberFormat.format(value, toAppendTo, dummyFieldPosition());
                if (suffix != null) {
                    toAppendTo.append(suffix);
                }
            }
            if (field == fieldPos) {
                pos.setBeginIndex(startPosition);
                pos.setEndIndex(toAppendTo.length());
            }
        } while (hasMore);
        return toAppendTo;
    }