    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.math3.fraction;
    018
    019    import java.math.BigInteger;
    020    import java.text.FieldPosition;
    021    import java.text.NumberFormat;
    022    import java.text.ParsePosition;
    023
    024    import org.apache.commons.math3.exception.util.LocalizedFormats;
    025    import org.apache.commons.math3.exception.NullArgumentException;
    026
    027    /**
    028     * Formats a BigFraction number in proper format.  The number format for each of
    029     * the whole number, numerator and, denominator can be configured.
    030     * <p>
    031     * Minus signs are only allowed in the whole number part - i.e.,
    032     * "-3 1/2" is legitimate and denotes -7/2, but "-3 -1/2" is invalid and
    033     * will result in a <code>ParseException</code>.</p>
    034     *
    035     * @since 1.1
    036     * @version $Id: ProperBigFractionFormat.java 1416643 2012-12-03 19:37:14Z tn $
    037     */
    038    public class ProperBigFractionFormat extends BigFractionFormat {
    039
    040        /** Serializable version identifier */
    041        private static final long serialVersionUID = -6337346779577272307L;
    042
    043        /** The format used for the whole number. */
    044        private NumberFormat wholeFormat;
    045
    046        /**
    047         * Create a proper formatting instance with the default number format for
    048         * the whole, numerator, and denominator.
    049         */
    050        public ProperBigFractionFormat() {
    051            this(getDefaultNumberFormat());
    052        }
    053
    054        /**
    055         * Create a proper formatting instance with a custom number format for the
    056         * whole, numerator, and denominator.
    057         * @param format the custom format for the whole, numerator, and
    058         *        denominator.
    059         */
    060        public ProperBigFractionFormat(final NumberFormat format) {
    061            this(format, (NumberFormat)format.clone(), (NumberFormat)format.clone());
    062        }
    063
    064        /**
    065         * Create a proper formatting instance with a custom number format for each
    066         * of the whole, numerator, and denominator.
    067         * @param wholeFormat the custom format for the whole.
    068         * @param numeratorFormat the custom format for the numerator.
    069         * @param denominatorFormat the custom format for the denominator.
    070         */
    071        public ProperBigFractionFormat(final NumberFormat wholeFormat,
    072                                       final NumberFormat numeratorFormat,
    073                                       final NumberFormat denominatorFormat) {
    074            super(numeratorFormat, denominatorFormat);
    075            setWholeFormat(wholeFormat);
    076        }
    077
    078        /**
    079         * Formats a {@link BigFraction} object to produce a string.  The BigFraction
    080         * is output in proper format.
    081         *
    082         * @param fraction the object to format.
    083         * @param toAppendTo where the text is to be appended
    084         * @param pos On input: an alignment field, if desired. On output: the
    085         *            offsets of the alignment field
    086         * @return the value passed in as toAppendTo.
    087         */
    088        @Override
    089        public StringBuffer format(final BigFraction fraction,
    090                                   final StringBuffer toAppendTo, final FieldPosition pos) {
    091
    092            pos.setBeginIndex(0);
    093            pos.setEndIndex(0);
    094
    095            BigInteger num = fraction.getNumerator();
    096            BigInteger den = fraction.getDenominator();
    097            BigInteger whole = num.divide(den);
    098            num = num.remainder(den);
    099
    100            if (!BigInteger.ZERO.equals(whole)) {
    101                getWholeFormat().format(whole, toAppendTo, pos);
    102                toAppendTo.append(' ');
    103                if (num.compareTo(BigInteger.ZERO) < 0) {
    104                    num = num.negate();
    105                }
    106            }
    107            getNumeratorFormat().format(num, toAppendTo, pos);
    108            toAppendTo.append(" / ");
    109            getDenominatorFormat().format(den, toAppendTo, pos);
    110
    111            return toAppendTo;
    112        }
    113
    114        /**
    115         * Access the whole format.
    116         * @return the whole format.
    117         */
    118        public NumberFormat getWholeFormat() {
    119            return wholeFormat;
    120        }
    121
    122        /**
    123         * Parses a string to produce a {@link BigFraction} object.  This method
    124         * expects the string to be formatted as a proper BigFraction.
    125         * <p>
    126         * Minus signs are only allowed in the whole number part - i.e.,
    127         * "-3 1/2" is legitimate and denotes -7/2, but "-3 -1/2" is invalid and
    128         * will result in a <code>ParseException</code>.</p>
    129         *
    130         * @param source the string to parse
    131         * @param pos input/ouput parsing parameter.
    132         * @return the parsed {@link BigFraction} object.
    133         */
    134        @Override
    135        public BigFraction parse(final String source, final ParsePosition pos) {
    136            // try to parse improper BigFraction
    137            BigFraction ret = super.parse(source, pos);
    138            if (ret != null) {
    139                return ret;
    140            }
    141
    142            final int initialIndex = pos.getIndex();
    143
    144            // parse whitespace
    145            parseAndIgnoreWhitespace(source, pos);
    146
    147            // parse whole
    148            BigInteger whole = parseNextBigInteger(source, pos);
    149            if (whole == null) {
    150                // invalid integer number
    151                // set index back to initial, error index should already be set
    152                // character examined.
    153                pos.setIndex(initialIndex);
    154                return null;
    155            }
    156
    157            // parse whitespace
    158            parseAndIgnoreWhitespace(source, pos);
    159
    160            // parse numerator
    161            BigInteger num = parseNextBigInteger(source, pos);
    162            if (num == null) {
    163                // invalid integer number
    164                // set index back to initial, error index should already be set
    165                // character examined.
    166                pos.setIndex(initialIndex);
    167                return null;
    168            }
    169
    170            if (num.compareTo(BigInteger.ZERO) < 0) {
    171                // minus signs should be leading, invalid expression
    172                pos.setIndex(initialIndex);
    173                return null;
    174            }
    175
    176            // parse '/'
    177            final int startIndex = pos.getIndex();
    178            final char c = parseNextCharacter(source, pos);
    179            switch (c) {
    180            case 0 :
    181                // no '/'
    182                // return num as a BigFraction
    183                return new BigFraction(num);
    184            case '/' :
    185                // found '/', continue parsing denominator
    186                break;
    187            default :
    188                // invalid '/'
    189                // set index back to initial, error index should be the last
    190                // character examined.
    191                pos.setIndex(initialIndex);
    192                pos.setErrorIndex(startIndex);
    193                return null;
    194            }
    195
    196            // parse whitespace
    197            parseAndIgnoreWhitespace(source, pos);
    198
    199            // parse denominator
    200            final BigInteger den = parseNextBigInteger(source, pos);
    201            if (den == null) {
    202                // invalid integer number
    203                // set index back to initial, error index should already be set
    204                // character examined.
    205                pos.setIndex(initialIndex);
    206                return null;
    207            }
    208
    209            if (den.compareTo(BigInteger.ZERO) < 0) {
    210                // minus signs must be leading, invalid
    211                pos.setIndex(initialIndex);
    212                return null;
    213            }
    214
    215            boolean wholeIsNeg = whole.compareTo(BigInteger.ZERO) < 0;
    216            if (wholeIsNeg) {
    217                whole = whole.negate();
    218            }
    219            num = whole.multiply(den).add(num);
    220            if (wholeIsNeg) {
    221                num = num.negate();
    222            }
    223
    224            return new BigFraction(num, den);
    225
    226        }
    227
    228        /**
    229         * Modify the whole format.
    230         * @param format The new whole format value.
    231         * @throws NullArgumentException if {@code format} is {@code null}.
    232         */
    233        public void setWholeFormat(final NumberFormat format) {
    234            if (format == null) {
    235                throw new NullArgumentException(LocalizedFormats.WHOLE_FORMAT);
    236            }
    237            this.wholeFormat = format;
    238        }
    239    }