001/*
002 * JScience - Java(TM) Tools and Libraries for the Advancement of Sciences.
003 * Copyright (C) 2006 - JScience (http://jscience.org/)
004 * All rights reserved.
005 * 
006 * Permission to use, copy, modify, and distribute this software is
007 * freely granted, provided that this notice is preserved.
008 */
009package org.jscience.physics.amount;
010
011import java.io.IOException;
012import java.text.ParseException;
013
014import org.jscience.economics.money.Currency;
015import org.jscience.economics.money.Money;
016
017import javax.measure.unit.Unit;
018import javax.measure.unit.UnitFormat;
019
020import javolution.lang.MathLib;
021import javolution.text.Text;
022import javolution.text.TextBuilder;
023import javolution.text.TextFormat;
024import javolution.text.TypeFormat;
025import javolution.context.LocalContext;
026
027//@RETROWEAVER import javolution.text.Appendable;
028
029/**
030 * <p> This class provides the interface for formatting and parsing {@link 
031 *     Amount measures} instances. For example:[code]
032 *     // Display measurements using unscaled units (e.g. base units or alternate units).
033 *     AmountFormat.setInstance(new AmountFormat() { // Context local.
034 *         public Appendable format(Amount m, Appendable a) throws IOException {
035 *             Unit u = m.getUnit();
036 *             if (u instanceof TransformedUnit)
037 *                   u = ((TransformedUnit)u).getParentUnit();
038 *             return AmountFormat.getPlusMinusErrorInstance(2).format(m.to(u), a);
039 *         }
040 *         public Amount parse(CharSequence csq, Cursor c) {
041 *             return AmountFormat.getPlusMinusErrorInstance(2).parse(csq, c);
042 *         }
043 *     });[/code]
044 *     </p>
045 *     
046 * @author  <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a>
047 * @version 3.0, February 21, 2006
048 */
049public abstract class AmountFormat extends TextFormat<Amount<?>> {
050
051    /**
052     * Holds current format.
053     */
054    private static final LocalContext.Reference<AmountFormat> CURRENT = new LocalContext.Reference<AmountFormat>(
055            new PlusMinusError(2));
056
057    /**
058     * Default constructor.
059     */
060    protected AmountFormat() {
061    }
062
063    /**
064     * Returns the current {@link javolution.context.LocalContext local}  
065     * format (default <code>AmountFormat.getPlusMinusErrorInstance(2)</code>).
066     *
067     * @return the context local format.
068     * @see #getPlusMinusErrorInstance(int)
069     */
070    public static AmountFormat getInstance() {
071        return CURRENT.get();
072    }
073
074    /**
075     * Sets the current {@link javolution.context.LocalContext local} format.
076     *
077     * @param format the new format.
078     */
079    public static void setInstance(AmountFormat format) {
080        CURRENT.set(format);
081    }
082
083    /**
084     * Returns a format for which the error (if present) is stated using 
085     * the '±' character; for example <code>"(1.34 ± 0.01) m"</code>.
086     * This format can be used for formatting as well as for parsing.
087     * 
088     * @param digitsInError the maximum number of digits in error.
089     */
090    public static AmountFormat getPlusMinusErrorInstance(int digitsInError) {
091        return new PlusMinusError(digitsInError);
092    }
093
094    /**
095     * Returns a format for which the error is represented by an integer 
096     * value in brackets; for example <code>"1.3456[20] m"</code> 
097     * is equivalent to <code>"1.3456 ± 0.0020 m"</code>. 
098     * This format can be used for formatting as well as for parsing.
099     * 
100     * @param digitsInError the maximum number of digits in error.
101     */
102    public static AmountFormat getBracketErrorInstance(int digitsInError) {
103        return new BracketError(digitsInError);
104    }
105
106    /**
107     * Returns a format for which only digits guaranteed to be exact are 
108     * written out. In other words, the error is always on the
109     * last digit and less than the last digit weight. For example,
110     * <code>"1.34 m"</code> means a length between <code>1.32 m</code> and
111     * <code>1.35 m</code>. This format can be used for formatting only.
112     */
113    public static AmountFormat getExactDigitsInstance() {
114        return new ExactDigitsOnly();
115    }
116
117    /**
118     * This class represents the plus minus error format.
119     */
120    private static class PlusMinusError extends AmountFormat {
121
122        /**
123         * Holds the number of digits in error.
124         */
125        private int _errorDigits;
126
127        /**
128         * Creates a plus-minus error format having the specified 
129         * number of digits in error.
130         * 
131         * @param errorDigits the number of digits in error.
132         */
133        private PlusMinusError(int errorDigits) {
134            _errorDigits = errorDigits;
135        }
136
137        @SuppressWarnings("unchecked")
138        @Override
139        public Appendable format(Amount arg0, Appendable arg1)
140                throws IOException {
141            if (arg0.getUnit() instanceof Currency)
142                return formatMoney(arg0, arg1);
143            if (arg0.isExact()) {
144                TypeFormat.format(arg0.getExactValue(), arg1);
145                arg1.append(' ');
146                return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
147            }
148            double value = arg0.getEstimatedValue();
149            double error = arg0.getAbsoluteError();
150            int log10Value = (int) MathLib.floor(MathLib.log10(MathLib
151                    .abs(value)));
152            int log10Error = (int) MathLib.floor(MathLib.log10(error));
153            int digits = log10Value - log10Error - 1; // Exact digits.
154            digits = MathLib.max(1, digits + _errorDigits);
155
156            boolean scientific = (MathLib.abs(value) >= 1E6)
157                    || (MathLib.abs(value) < 1E-6);
158            boolean showZeros = false;
159            arg1.append('(');
160            TypeFormat.format(value, digits, scientific, showZeros, arg1);
161            arg1.append(" ± ");
162            scientific = (MathLib.abs(error) >= 1E6)
163                    || (MathLib.abs(error) < 1E-6);
164            showZeros = true;
165            TypeFormat.format(error, _errorDigits, scientific, showZeros, arg1);
166            arg1.append(") ");
167            return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
168        }
169
170        @Override
171        public Amount<?> parse(CharSequence arg0, Cursor arg1) {
172            int start = arg1.getIndex();
173            try {
174                arg1.skip('(', arg0);
175                long value = TypeFormat.parseLong(arg0, 10, arg1);
176                if (arg0.charAt(arg1.getIndex()) == ' ') { // Exact! 
177                    arg1.skip(' ', arg0);
178                    Unit<?> unit = UnitFormat.getInstance().parseProductUnit(
179                            arg0, arg1);
180                    return Amount.valueOf(value, unit);
181                }
182                arg1.setIndex(start);
183                double amount = TypeFormat.parseDouble(arg0, arg1);
184                arg1.skip(' ', arg0);
185                double error = 0;
186                if (arg0.charAt(arg1.getIndex()) == '±') { // Error specified. 
187                    arg1.skip('±', arg0);
188                    arg1.skip(' ', arg0);
189                    error = TypeFormat.parseDouble(arg0, arg1);
190                }
191                arg1.skip(')', arg0);
192                arg1.skip(' ', arg0);
193                Unit<?> unit = UnitFormat.getInstance().parseProductUnit(arg0,
194                        arg1);
195                return Amount.valueOf(amount, error, unit);
196            } catch (ParseException e) {
197                arg1.setIndex(start);
198                arg1.setErrorIndex(e.getErrorOffset());
199                return null;
200            }
201        }
202    }
203
204    /**
205     * This class represents the bracket error format.
206     */
207    private static class BracketError extends AmountFormat {
208
209        /**
210         * Holds the number of digits in error.
211         */
212        private int _errorDigits;
213
214        /**
215         * Creates a bracket error format having the specified 
216         * number of digits in error.
217         * 
218         * @param bracket the number of digits in error.
219         */
220        private BracketError(int errorDigits) {
221            _errorDigits = errorDigits;
222        }
223
224        @SuppressWarnings("unchecked")
225        @Override
226        public Appendable format(Amount arg0, Appendable arg1)
227                throws IOException {
228            if (arg0.getUnit() instanceof Currency)
229                return formatMoney(arg0, arg1);
230            if (arg0.isExact()) {
231                TypeFormat.format(arg0.getExactValue(), arg1);
232                arg1.append(' ');
233                return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
234            }
235            double value = arg0.getEstimatedValue();
236            double error = arg0.getAbsoluteError();
237            int log10Value = (int) MathLib.floor(MathLib.log10(MathLib
238                    .abs(value)));
239            int log10Error = (int) MathLib.floor(MathLib.log10(error));
240            int digits = log10Value - log10Error - 1; // Exact digits.
241            digits = MathLib.max(1, digits + _errorDigits);
242
243            boolean scientific = (MathLib.abs(value) >= 1E6)
244                    || (MathLib.abs(value) < 1E-6);
245            boolean showZeros = true;
246            TextBuilder tb = TextBuilder.newInstance();
247            TypeFormat.format(value, digits, scientific, showZeros, tb);
248            int endMantissa = 0;
249            for (; endMantissa < tb.length(); endMantissa++) {
250                if (tb.charAt(endMantissa) == 'E')
251                    break;
252            }
253            int bracketError = (int) (error * MathLib.toDoublePow10(1,
254                    -log10Error + _errorDigits - 1));
255            tb.insert(endMantissa, Text.valueOf('[').plus(
256                    Text.valueOf(bracketError)).plus(']'));
257            arg1.append(tb);
258            arg1.append(' ');
259            return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
260        }
261
262        @Override
263        public Amount<?> parse(CharSequence arg0, Cursor arg1) {
264            // TBD
265            throw new UnsupportedOperationException("Not supported yet");
266        }
267    }
268
269    /**
270     * This class represents the exact digits only format.
271     */
272    private static class ExactDigitsOnly extends AmountFormat {
273
274        /**
275         * Default constructor.
276         */
277        private ExactDigitsOnly() {
278        }
279
280        @SuppressWarnings("unchecked")
281        @Override
282        public Appendable format(Amount arg0, Appendable arg1)
283                throws IOException {
284            if (arg0.getUnit() instanceof Currency)
285                return formatMoney(arg0, arg1);
286            if (arg0.isExact()) {
287                TypeFormat.format(arg0.getExactValue(), arg1);
288                arg1.append(' ');
289                return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
290            }
291            double value = arg0.getEstimatedValue();
292            double error = arg0.getAbsoluteError();
293            int log10Value = (int) MathLib.floor(MathLib.log10(MathLib
294                    .abs(value)));
295            int log10Error = (int) MathLib.floor(MathLib.log10(error));
296            int digits = log10Value - log10Error - 1; // Exact digits.
297
298            boolean scientific = (MathLib.abs(value) >= 1E6)
299                    || (MathLib.abs(value) < 1E-6);
300            boolean showZeros = true;
301            TypeFormat.format(value, digits, scientific, showZeros, arg1);
302            arg1.append(' ');
303            return UnitFormat.getInstance().format(arg0.getUnit(), arg1);
304        }
305
306        @Override
307        public Amount<?> parse(CharSequence arg0, Cursor arg1) {
308            throw new UnsupportedOperationException(
309                    "This format should not be used for parsing "
310                            + "(not enough information on the error");
311        }
312    }
313
314    /**
315     * Provides custom formatting for money measurements.
316     */
317    private static Appendable formatMoney(Amount<Money> arg0, Appendable arg1)
318            throws IOException {
319        Currency currency = (Currency) arg0.getUnit();
320        int fraction = currency.getDefaultFractionDigits();
321        if (fraction == 0) {
322            long amount = arg0.longValue(currency);
323            TypeFormat.format(amount, arg1);
324        } else if (fraction == 2) {
325            long amount = MathLib.round(arg0.doubleValue(arg0.getUnit()) * 100);
326            TypeFormat.format(amount / 100, arg1);
327            arg1.append('.');
328            arg1.append((char) ('0' + (amount % 100) / 10));
329            arg1.append((char) ('0' + (amount % 10)));
330        } else {
331            throw new UnsupportedOperationException();
332        }
333        arg1.append(' ');
334        return UnitFormat.getInstance().format(currency, arg1);
335    }
336
337}