1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17package org.apache.commons.math.fraction;
18
19import java.text.FieldPosition;
20import java.text.NumberFormat;
21import java.text.ParsePosition;
22
23import org.apache.commons.math.exception.util.LocalizedFormats;
24import org.apache.commons.math.exception.NullArgumentException;
25import org.apache.commons.math.util.MathUtils;
26
27/**
28 * Formats a Fraction number in proper format.  The number format for each of
29 * the whole number, numerator and, denominator can be configured.
30 * <p>
31 * Minus signs are only allowed in the whole number part - i.e.,
32 * "-3 1/2" is legitimate and denotes -7/2, but "-3 -1/2" is invalid and
33 * will result in a <code>ParseException</code>.</p>
34 *
35 * @since 1.1
36 * @version $Revision: 983921 $ $Date: 2010-08-10 12:46:06 +0200 (mar. 10 août 2010) $
37 */
38public class ProperFractionFormat extends FractionFormat {
39
40    /** Serializable version identifier */
41    private static final long serialVersionUID = 760934726031766749L;
42
43    /** The format used for the whole number. */
44    private NumberFormat wholeFormat;
45
46    /**
47     * Create a proper formatting instance with the default number format for
48     * the whole, numerator, and denominator.
49     */
50    public ProperFractionFormat() {
51        this(getDefaultNumberFormat());
52    }
53
54    /**
55     * Create a proper formatting instance with a custom number format for the
56     * whole, numerator, and denominator.
57     * @param format the custom format for the whole, numerator, and
58     *        denominator.
59     */
60    public ProperFractionFormat(NumberFormat format) {
61        this(format, (NumberFormat)format.clone(), (NumberFormat)format.clone());
62    }
63
64    /**
65     * Create a proper formatting instance with a custom number format for each
66     * of the whole, numerator, and denominator.
67     * @param wholeFormat the custom format for the whole.
68     * @param numeratorFormat the custom format for the numerator.
69     * @param denominatorFormat the custom format for the denominator.
70     */
71    public ProperFractionFormat(NumberFormat wholeFormat,
72            NumberFormat numeratorFormat,
73            NumberFormat denominatorFormat)
74    {
75        super(numeratorFormat, denominatorFormat);
76        setWholeFormat(wholeFormat);
77    }
78
79    /**
80     * Formats a {@link Fraction} object to produce a string.  The fraction
81     * is output in proper format.
82     *
83     * @param fraction the object to format.
84     * @param toAppendTo where the text is to be appended
85     * @param pos On input: an alignment field, if desired. On output: the
86     *            offsets of the alignment field
87     * @return the value passed in as toAppendTo.
88     */
89    @Override
90    public StringBuffer format(Fraction fraction, StringBuffer toAppendTo,
91            FieldPosition pos) {
92
93        pos.setBeginIndex(0);
94        pos.setEndIndex(0);
95
96        int num = fraction.getNumerator();
97        int den = fraction.getDenominator();
98        int whole = num / den;
99        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.sign(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}
233