    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.text.FieldPosition;
    020    import java.text.NumberFormat;
    021    import java.text.ParsePosition;
    022
    023    import org.apache.commons.math3.exception.util.LocalizedFormats;
    024    import org.apache.commons.math3.exception.NullArgumentException;
    025    import org.apache.commons.math3.util.MathUtils;
    026
    027    /**
    028     * Formats a Fraction 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: ProperFractionFormat.java 1416643 2012-12-03 19:37:14Z tn $
    037     */
    038    public class ProperFractionFormat extends FractionFormat {
    039
    040        /** Serializable version identifier */
    041        private static final long serialVersionUID = 760934726031766749L;
    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 ProperFractionFormat() {
    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 ProperFractionFormat(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 ProperFractionFormat(NumberFormat wholeFormat,
    072                NumberFormat numeratorFormat,
    073                NumberFormat denominatorFormat)
    074        {
    075            super(numeratorFormat, denominatorFormat);
    076            setWholeFormat(wholeFormat);
    077        }
    078
    079        /**
    080         * Formats a {@link Fraction} object to produce a string.  The fraction
    081         * is output in proper format.
    082         *
    083         * @param fraction the object to format.
    084         * @param toAppendTo where the text is to be appended
    085         * @param pos On input: an alignment field, if desired. On output: the
    086         *            offsets of the alignment field
    087         * @return the value passed in as toAppendTo.
    088         */
    089        @Override
    090        public StringBuffer format(Fraction fraction, StringBuffer toAppendTo,
    091                FieldPosition pos) {
    092
    093            pos.setBeginIndex(0);
    094            pos.setEndIndex(0);
    095
    096            int num = fraction.getNumerator();
    097            int den = fraction.getDenominator();
    098            int whole = num / den;
    099            num = num % den;
    100
    101            if (whole != 0) {
    102                getWholeFormat().format(whole, toAppendTo, pos);
    103                toAppendTo.append(' ');
    104                num = Math.abs(num);
    105            }
    106            getNumeratorFormat().format(num, toAppendTo, pos);
    107            toAppendTo.append(" / ");
    108            getDenominatorFormat().format(den, toAppendTo,
    109                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 Fraction} object.  This method
    124         * expects the string to be formatted as a proper fraction.
    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 Fraction} object.
    133         */
    134        @Override
    135        public Fraction parse(String source, ParsePosition pos) {
    136            // try to parse improper fraction
    137            Fraction ret = super.parse(source, pos);
    138            if (ret != null) {
    139                return ret;
    140            }
    141
    142            int initialIndex = pos.getIndex();
    143
    144            // parse whitespace
    145            parseAndIgnoreWhitespace(source, pos);
    146
    147            // parse whole
    148            Number whole = getWholeFormat().parse(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            Number num = getNumeratorFormat().parse(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.intValue() < 0) {
    171                // minus signs should be leading, invalid expression
    172                pos.setIndex(initialIndex);
    173                return null;
    174            }
    175
    176            // parse '/'
    177            int startIndex = pos.getIndex();
    178            char c = parseNextCharacter(source, pos);
    179            switch (c) {
    180            case 0 :
    181                // no '/'
    182                // return num as a fraction
    183                return new Fraction(num.intValue(), 1);
    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            Number den = getDenominatorFormat().parse(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.intValue() < 0) {
    210                // minus signs must be leading, invalid
    211                pos.setIndex(initialIndex);
    212                return null;
    213            }
    214
    215            int w = whole.intValue();
    216            int n = num.intValue();
    217            int d = den.intValue();
    218            return new Fraction(((Math.abs(w) * d) + n) * MathUtils.copySign(1, w), d);
    219        }
    220
    221        /**
    222         * Modify the whole format.
    223         * @param format The new whole format value.
    224         * @throws NullArgumentException if {@code format} is {@code null}.
    225         */
    226        public void setWholeFormat(NumberFormat format) {
    227            if (format == null) {
    228                throw new NullArgumentException(LocalizedFormats.WHOLE_FORMAT);
    229            }
    230            this.wholeFormat = format;
    231        }
    232    }