← joda-money  /  src/main/java/org/joda/money/format/MoneyFormatter.java

1
/*
2
 *  Copyright 2009-present, Stephen Colebourne
3
 *
4
 *  Licensed under the Apache License, Version 2.0 (the "License");
5
 *  you may not use this file except in compliance with the License.
6
 *  You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 *  Unless required by applicable law or agreed to in writing, software
11
 *  distributed under the License is distributed on an "AS IS" BASIS,
12
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 *  See the License for the specific language governing permissions and
14
 *  limitations under the License.
15
 */
16
package org.joda.money.format;
17
18
import java.io.IOException;
19
import java.io.Serializable;
20
import java.util.Locale;
21
22
import org.joda.money.BigMoney;
23
import org.joda.money.BigMoneyProvider;
24
import org.joda.money.Money;
25
26
/**
27
 * Formats instances of money to and from a String.
28
 * <p>
29
 * Instances of {@code MoneyFormatter} can be created by
30
 * {@code MoneyFormatterBuilder}.
31
 * <p>
32
 * This class is immutable and thread-safe.
33
 */
34
public final class MoneyFormatter implements Serializable {
35
36
    /**
37
     * Serialization version.
38
     */
39
    private static final long serialVersionUID = 2385346258L;
40
41
    /**
42
     * The locale to use.
43
     */
44
    private final Locale locale;
45
    /**
46
     * The printer/parser.
47
     */
48
    private final MultiPrinterParser printerParser;
49
50
    //-----------------------------------------------------------------------
51
    /**
52
     * Validates that the object specified is not null
53
     *
54
     * @param object  the object to check, null throws exception
55
     * @param message  the message to use in the exception, not null
56
     * @throws NullPointerException if the input value is null
57
     */
58
    static void checkNotNull(Object object, String message) {
59
        if (object == null) {
60
            throw new NullPointerException(message);
61
        }
62
    }
63
64
    //-----------------------------------------------------------------------
65
    /**
66
     * Constructor, creating a new formatter.
67
     *
68
     * @param locale  the locale to use, not null
69
     * @param printers  the printers, not null
70
     * @param parsers  the parsers, not null
71
     */
72
    MoneyFormatter(Locale locale, MoneyPrinter[] printers, MoneyParser[] parsers) {
73
        MoneyFormatter.checkNotNull(locale, "Locale must not be null");
74
        MoneyFormatter.checkNotNull(printers, "Printers must not be null");
75
        MoneyFormatter.checkNotNull(parsers, "Parsers must not be null");
76
        if (printers.length != parsers.length) {
77
            throw new IllegalArgumentException("Printers and parsers must match");
78
        }
79
        this.locale = locale;
80
        this.printerParser = new MultiPrinterParser(printers, parsers);
81
    }
82
83
    /**
84
     * Constructor, creating a new formatter.
85
     *
86
     * @param locale  the locale to use, not null
87
     * @param printerParser  the printer/parser, not null
88
     */
89
    private MoneyFormatter(Locale locale, MultiPrinterParser printerParser) {
90
        MoneyFormatter.checkNotNull(locale, "Locale must not be null");
91
        MoneyFormatter.checkNotNull(printerParser, "PrinterParser must not be null");
92
        this.locale = locale;
93
        this.printerParser = printerParser;
94
    }
95
96
    //-----------------------------------------------------------------------
97
    /**
98
     * Gets the printer/parser.
99
     *
100
     * @return the printer/parser, never null
101
     */
102
    MultiPrinterParser getPrinterParser() {
103
        return printerParser;
104
    }
105
106
    //-----------------------------------------------------------------------
107
    /**
108
     * Gets the locale to use.
109
     *
110
     * @return the locale, never null
111
     */
112
    public Locale getLocale() {
113
        return locale;
114
    }
115
116
    /**
117
     * Returns a copy of this instance with the specified locale.
118
     * <p>
119
     * Changing the locale may change the style of output depending on how the
120
     * formatter has been configured.
121
     *
122
     * @param locale  the locale, not null
123
     * @return the new instance, never null
124
     */
125
    public MoneyFormatter withLocale(Locale locale) {
126
        checkNotNull(locale, "Locale must not be null");
127
        return new MoneyFormatter(locale, printerParser);
128
    }
129
130
    //-----------------------------------------------------------------------
131
    /**
132
     * Checks whether this formatter can print.
133
     * <p>
134
     * If the formatter cannot print, an UnsupportedOperationException will
135
     * be thrown from the print methods.
136
     *
137
     * @return true if the formatter can print
138
     */
139
    public boolean isPrinter() {
140
        return printerParser.isPrinter();
141
    }
142
143
    /**
144
     * Checks whether this formatter can parse.
145
     * <p>
146
     * If the formatter cannot parse, an UnsupportedOperationException will
147
     * be thrown from the parse methods.
148
     *
149
     * @return true if the formatter can parse
150
     */
151
    public boolean isParser() {
152
        return printerParser.isParser();
153
    }
154
155
    //-----------------------------------------------------------------------
156
    /**
157
     * Prints a monetary value to a {@code String}.
158
     *
159
     * @param moneyProvider  the money to print, not null
160
     * @return the string printed using the settings of this formatter
161
     * @throws UnsupportedOperationException if the formatter is unable to print
162
     * @throws MoneyFormatException if there is a problem while printing
163
     */
164
    public String print(BigMoneyProvider moneyProvider) {
165
        var buf = new StringBuilder();
166
        print(buf, moneyProvider);
167
        return buf.toString();
168
    }
169
170
    /**
171
     * Prints a monetary value to an {@code Appendable} converting
172
     * any {@code IOException} to a {@code MoneyFormatException}.
173
     * <p>
174
     * Example implementations of {@code Appendable} are {@code StringBuilder},
175
     * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
176
     * and {@code StringBuffer} never throw an {@code IOException}.
177
     *
178
     * @param appendable  the appendable to add to, not null
179
     * @param moneyProvider  the money to print, not null
180
     * @throws UnsupportedOperationException if the formatter is unable to print
181
     * @throws MoneyFormatException if there is a problem while printing
182
     */
183
    public void print(Appendable appendable, BigMoneyProvider moneyProvider) {
184
        try {
185
            printIO(appendable, moneyProvider);
186
        } catch (IOException ex) {
187
            throw new MoneyFormatException(ex.getMessage(), ex);
188
        }
189
    }
190
191
    /**
192
     * Prints a monetary value to an {@code Appendable} potentially
193
     * throwing an {@code IOException}.
194
     * <p>
195
     * Example implementations of {@code Appendable} are {@code StringBuilder},
196
     * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
197
     * and {@code StringBuffer} never throw an {@code IOException}.
198
     *
199
     * @param appendable  the appendable to add to, not null
200
     * @param moneyProvider  the money to print, not null
201
     * @throws UnsupportedOperationException if the formatter is unable to print
202
     * @throws MoneyFormatException if there is a problem while printing
203
     * @throws IOException if an IO error occurs
204
     */
205
    public void printIO(Appendable appendable, BigMoneyProvider moneyProvider) throws IOException {
206
        checkNotNull(moneyProvider, "BigMoneyProvider must not be null");
207
        if (!isPrinter()) {
208
            throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to print");
209
        }
210
211
        var money = BigMoney.of(moneyProvider);
212
        var context = new MoneyPrintContext(locale);
213
        printerParser.print(context, appendable, money);
214
    }
215
216
    //-----------------------------------------------------------------------
217
    /**
218
     * Fully parses the text into a {@code BigMoney}.
219
     * <p>
220
     * The parse must complete normally and parse the entire text (currency and amount).
221
     * If the parse completes without reading the entire length of the text, an exception is thrown.
222
     * If any other problem occurs during parsing, an exception is thrown.
223
     *
224
     * @param text  the text to parse, not null
225
     * @return the parsed monetary value, never null
226
     * @throws UnsupportedOperationException if the formatter is unable to parse
227
     * @throws MoneyFormatException if there is a problem while parsing
228
     */
229
    public BigMoney parseBigMoney(CharSequence text) {
230
        checkNotNull(text, "Text must not be null");
231
        var result = parse(text, 0);
232
        if (result.isError() || !result.isFullyParsed() || !result.isComplete()) {
233
            var str = (text.length() > 64 ? text.subSequence(0, 64).toString() + "..." : text.toString());
234
            if (result.isError()) {
235
                throw new MoneyFormatException("Text could not be parsed at index " + result.getErrorIndex() + ": " + str);
236
            } else if (!result.isFullyParsed()) {
237
                throw new MoneyFormatException("Unparsed text found at index " + result.getIndex() + ": " + str);
238
            } else {
239
                throw new MoneyFormatException("Parsing did not find both currency and amount: " + str);
240
            }
241
        }
242
        return result.toBigMoney();
243
    }
244
245
    /**
246
     * Fully parses the text into a {@code Money} requiring that the parsed
247
     * amount has the correct number of decimal places.
248
     * <p>
249
     * The parse must complete normally and parse the entire text (currency and amount).
250
     * If the parse completes without reading the entire length of the text, an exception is thrown.
251
     * If any other problem occurs during parsing, an exception is thrown.
252
     *
253
     * @param text  the text to parse, not null
254
     * @return the parsed monetary value, never null
255
     * @throws UnsupportedOperationException if the formatter is unable to parse
256
     * @throws MoneyFormatException if there is a problem while parsing
257
     * @throws ArithmeticException if the scale of the parsed money exceeds the scale of the currency
258
     */
259
    public Money parseMoney(CharSequence text) {
260
        return parseBigMoney(text).toMoney();
261
    }
262
263
    /**
264
     * Parses the text extracting monetary information.
265
     * <p>
266
     * This method parses the input providing low-level access to the parsing state.
267
     * The resulting context contains the parsed text, indicator of error, position
268
     * following the parse and the parsed currency and amount.
269
     * Together, these provide enough information for higher level APIs to use.
270
     *
271
     * @param text  the text to parse, not null
272
     * @param startIndex  the start index to parse from
273
     * @return the parsed monetary value, null only if the parse results in an error
274
     * @throws IndexOutOfBoundsException if the start index is invalid
275
     * @throws UnsupportedOperationException if this formatter cannot parse
276
     */
277
    public MoneyParseContext parse(CharSequence text, int startIndex) {
278
        checkNotNull(text, "Text must not be null");
279
        if (startIndex < 0 || startIndex > text.length()) {
280
            throw new StringIndexOutOfBoundsException("Invalid start index: " + startIndex);
281
        }
282
        if (!isParser()) {
283
            throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to parse");
284
        }
285
        var context = new MoneyParseContext(locale, text, startIndex);
286
        printerParser.parse(context);
287
        return context;
288
    }
289
290
    //-----------------------------------------------------------------------
291
    /**
292
     * Gets a string summary of the formatter.
293
     *
294
     * @return a string summarising the formatter, never null
295
     */
296
    @Override
297
    public String toString() {
298
        return printerParser.toString();
299
    }
300
301
}
302