← joda-money  /  src/main/java/org/joda/money/format/MoneyFormatterBuilder.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.util.ArrayList;
20
import java.util.List;
21
import java.util.Locale;
22
23
import org.joda.money.BigMoney;
24
import org.joda.money.CurrencyUnit;
25
import org.joda.money.IllegalCurrencyException;
26
27
/**
28
 * Provides the ability to build a formatter for monetary values.
29
 * <p>
30
 * This class is mutable and intended for use by a single thread.
31
 * A new instance should be created for each use.
32
 * The formatters produced by the builder are immutable and thread-safe.
33
 */
34
public final class MoneyFormatterBuilder {
35
36
    /**
37
     * The printers.
38
     */
39
    private final List<MoneyPrinter> printers = new ArrayList<>();
40
    /**
41
     * The parsers.
42
     */
43
    private final List<MoneyParser> parsers = new ArrayList<>();
44
45
    //-----------------------------------------------------------------------
46
    /**
47
     * Constructor, creating a new empty builder.
48
     */
49
    public MoneyFormatterBuilder() {
50
    }
51
52
    //-----------------------------------------------------------------------
53
    /**
54
     * Appends the amount to the builder using a standard format.
55
     * <p>
56
     * The format used is {@link MoneyAmountStyle#ASCII_DECIMAL_POINT_GROUP3_COMMA}.
57
     * The amount is the value itself, such as '12.34'.
58
     *
59
     * @return this, for chaining, never null
60
     */
61
    public MoneyFormatterBuilder appendAmount() {
62
        var pp = new AmountPrinterParser(MoneyAmountStyle.ASCII_DECIMAL_POINT_GROUP3_COMMA);
63
        return appendInternal(pp, pp);
64
    }
65
66
    /**
67
     * Appends the amount to the builder using a grouped localized format.
68
     * <p>
69
     * The format used is {@link MoneyAmountStyle#LOCALIZED_GROUPING}.
70
     * The amount is the value itself, such as '12.34'.
71
     *
72
     * @return this, for chaining, never null
73
     */
74
    public MoneyFormatterBuilder appendAmountLocalized() {
75
        var pp = new AmountPrinterParser(MoneyAmountStyle.LOCALIZED_GROUPING);
76
        return appendInternal(pp, pp);
77
    }
78
79
    /**
80
     * Appends the amount to the builder using the specified amount style.
81
     * <p>
82
     * The amount is the value itself, such as '12.34'.
83
     * <p>
84
     * The amount style allows the formatting of the number to be controlled in detail.
85
     * This includes the characters for positive, negative, decimal, grouping and whether
86
     * to output the absolute or signed amount.
87
     * See {@link MoneyAmountStyle} for more details.
88
     *
89
     * @param style  the style to use, not null
90
     * @return this, for chaining, never null
91
     */
92
    public MoneyFormatterBuilder appendAmount(MoneyAmountStyle style) {
93
        MoneyFormatter.checkNotNull(style, "MoneyAmountStyle must not be null");
94
        var pp = new AmountPrinterParser(style);
95
        return appendInternal(pp, pp);
96
    }
97
98
    //-----------------------------------------------------------------------
99
    /**
100
     * Appends the currency code to the builder.
101
     * <p>
102
     * The currency code is the three letter ISO code, such as 'GBP'.
103
     *
104
     * @return this, for chaining, never null
105
     */
106
    public MoneyFormatterBuilder appendCurrencyCode() {
107
        return appendInternal(Singletons.CODE, Singletons.CODE);
108
    }
109
110
    /**
111
     * Appends the currency code to the builder.
112
     * <p>
113
     * The numeric code is the ISO numeric code, such as '826' and is
114
     * zero padded to three digits.
115
     *
116
     * @return this, for chaining, never null
117
     */
118
    public MoneyFormatterBuilder appendCurrencyNumeric3Code() {
119
        return appendInternal(Singletons.NUMERIC_3_CODE, Singletons.NUMERIC_3_CODE);
120
    }
121
122
    /**
123
     * Appends the currency code to the builder.
124
     * <p>
125
     * The numeric code is the ISO numeric code, such as '826'.
126
     *
127
     * @return this, for chaining, never null
128
     */
129
    public MoneyFormatterBuilder appendCurrencyNumericCode() {
130
        return appendInternal(Singletons.NUMERIC_CODE, Singletons.NUMERIC_CODE);
131
    }
132
133
    /**
134
     * Appends the localized currency symbol to the builder.
135
     * <p>
136
     * The localized currency symbol is the symbol as chosen by the locale
137
     * of the formatter.
138
     * <p>
139
     * Symbols cannot be parsed.
140
     *
141
     * @return this, for chaining, never null
142
     */
143
    public MoneyFormatterBuilder appendCurrencySymbolLocalized() {
144
        return appendInternal(SingletonPrinters.LOCALIZED_SYMBOL, null);
145
    }
146
147
    /**
148
     * Appends a literal to the builder.
149
     * <p>
150
     * The localized currency symbol is the symbol as chosen by the locale
151
     * of the formatter.
152
     *
153
     * @param literal  the literal to append, null or empty ignored
154
     * @return this, for chaining, never null
155
     */
156
    public MoneyFormatterBuilder appendLiteral(CharSequence literal) {
157
        if (literal == null || literal.length() == 0) {
158
            return this;
159
        }
160
        var pp = new LiteralPrinterParser(literal.toString());
161
        return appendInternal(pp, pp);
162
    }
163
164
    //-----------------------------------------------------------------------
165
    /**
166
     * Appends the printers and parsers from the specified formatter to this builder.
167
     * <p>
168
     * If the specified formatter cannot print, then the the output of this
169
     * builder will be unable to print. If the specified formatter cannot parse,
170
     * then the output of this builder will be unable to parse.
171
     *
172
     * @param formatter  the formatter to append, not null
173
     * @return this for chaining, never null
174
     */
175
    public MoneyFormatterBuilder append(MoneyFormatter formatter) {
176
        MoneyFormatter.checkNotNull(formatter, "MoneyFormatter must not be null");
177
        formatter.getPrinterParser().appendTo(this);
178
        return this;
179
    }
180
181
    /**
182
     * Appends the specified printer and parser to this builder.
183
     * <p>
184
     * If null is specified then the formatter will be unable to print/parse.
185
     *
186
     * @param printer  the printer to append, null makes the formatter unable to print
187
     * @param parser  the parser to append, null makes the formatter unable to parse
188
     * @return this for chaining, never null
189
     */
190
    public MoneyFormatterBuilder append(MoneyPrinter printer, MoneyParser parser) {
191
        return appendInternal(printer, parser);
192
    }
193
194
    //-----------------------------------------------------------------------
195
    /**
196
     * Appends the specified formatters, one used when the amount is positive,
197
     * and one when the amount is negative.
198
     * <p>
199
     * When printing, the amount is queried and the appropriate formatter is used.
200
     * <p>
201
     * When parsing, each formatter is tried, with the longest successful match,
202
     * or the first match if multiple are successful. If the negative parser is
203
     * matched, the amount returned will be negative no matter what amount is parsed.
204
     * <p>
205
     * A typical use case for this would be to produce a format like
206
     * '{@code ($123)}' for negative amounts and '{@code $123}' for positive amounts.
207
     * <p>
208
     * In order to use this method, it may be necessary to output an unsigned amount.
209
     * This can be achieved using {@link #appendAmount(MoneyAmountStyle)} and
210
     * {@link MoneyAmountStyle#withAbsValue(boolean)}.
211
     *
212
     * @param whenPositiveOrZero  the formatter to use when the amount is positive or zero
213
     * @param whenNegative  the formatter to use when the amount is negative
214
     * @return this for chaining, never null
215
     */
216
    public MoneyFormatterBuilder appendSigned(MoneyFormatter whenPositiveOrZero, MoneyFormatter whenNegative) {
217
        return appendSigned(whenPositiveOrZero, whenPositiveOrZero, whenNegative);
218
    }
219
220
    /**
221
     * Appends the specified formatters, one used when the amount is positive,
222
     * one when the amount is zero and one when the amount is negative.
223
     * <p>
224
     * When printing, the amount is queried and the appropriate formatter is used.
225
     * <p>
226
     * When parsing, each formatter is tried, with the longest successful match,
227
     * or the first match if multiple are successful. If the zero parser is matched,
228
     * the amount returned will be zero no matter what amount is parsed. If the negative
229
     * parser is matched, the amount returned will be negative no matter what amount is parsed.
230
     * <p>
231
     * A typical use case for this would be to produce a format like
232
     * '{@code ($123)}' for negative amounts and '{@code $123}' for positive amounts.
233
     * <p>
234
     * In order to use this method, it may be necessary to output an unsigned amount.
235
     * This can be achieved using {@link #appendAmount(MoneyAmountStyle)} and
236
     * {@link MoneyAmountStyle#withAbsValue(boolean)}.
237
     *
238
     * @param whenPositive  the formatter to use when the amount is positive
239
     * @param whenZero  the formatter to use when the amount is zero
240
     * @param whenNegative  the formatter to use when the amount is negative
241
     * @return this for chaining, never null
242
     */
243
    public MoneyFormatterBuilder appendSigned(MoneyFormatter whenPositive, MoneyFormatter whenZero, MoneyFormatter whenNegative) {
244
        MoneyFormatter.checkNotNull(whenPositive, "MoneyFormatter whenPositive must not be null");
245
        MoneyFormatter.checkNotNull(whenZero, "MoneyFormatter whenZero must not be null");
246
        MoneyFormatter.checkNotNull(whenNegative, "MoneyFormatter whenNegative must not be null");
247
        var pp = new SignedPrinterParser(whenPositive, whenZero, whenNegative);
248
        return appendInternal(pp, pp);
249
    }
250
251
    //-----------------------------------------------------------------------
252
    /**
253
     * Appends the specified printer and parser to this builder.
254
     * <p>
255
     * Either the printer or parser must be non-null.
256
     *
257
     * @param printer  the printer to append, null makes the formatter unable to print
258
     * @param parser  the parser to append, null makes the formatter unable to parse
259
     * @return this for chaining, never null
260
     */
261
    private MoneyFormatterBuilder appendInternal(MoneyPrinter printer, MoneyParser parser) {
262
        printers.add(printer);
263
        parsers.add(parser);
264
        return this;
265
    }
266
267
    //-----------------------------------------------------------------------
268
    /**
269
     * Builds the formatter from the builder using the default locale.
270
     * <p>
271
     * Once the builder is in the correct state it must be converted to a
272
     * {@code MoneyFormatter} to be used. Calling this method does not
273
     * change the state of this instance, so it can still be used.
274
     * <p>
275
     * This method uses the default locale within the returned formatter.
276
     * It can be changed by calling {@link MoneyFormatter#withLocale(Locale)}.
277
     *
278
     * @return the formatter built from this builder, never null
279
     */
280
    public MoneyFormatter toFormatter() {
281
        return toFormatter(Locale.getDefault());
282
    }
283
284
    /**
285
     * Builds the formatter from the builder setting the locale.
286
     * <p>
287
     * Once the builder is in the correct state it must be converted to a
288
     * {@code MoneyFormatter} to be used. Calling this method does not
289
     * change the state of this instance, so it can still be used.
290
     * <p>
291
     * This method uses the specified locale within the returned formatter.
292
     * It can be changed by calling {@link MoneyFormatter#withLocale(Locale)}.
293
     *
294
     * @param locale  the initial locale for the formatter, not null
295
     * @return the formatter built from this builder, never null
296
     */
297
    @SuppressWarnings("cast")
298
    public MoneyFormatter toFormatter(Locale locale) {
299
        MoneyFormatter.checkNotNull(locale, "Locale must not be null");
300
        var printersCopy = printers.toArray(new MoneyPrinter[printers.size()]);
301
        var parsersCopy = parsers.toArray(new MoneyParser[parsers.size()]);
302
        return new MoneyFormatter(locale, printersCopy, parsersCopy);
303
    }
304
305
    //-----------------------------------------------------------------------
306
    /**
307
     * Handles the singleton outputs.
308
     */
309
    private static enum Singletons implements MoneyPrinter, MoneyParser {
310
        CODE("${code}") {
311
            @Override
312
            public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {
313
                appendable.append(money.getCurrencyUnit().getCode());
314
            }
315
316
            @Override
317
            public void parse(MoneyParseContext context) {
318
                var endPos = context.getIndex() + 3;
319
                if (endPos > context.getTextLength()) {
320
                    context.setError();
321
                } else {
322
                    var code = context.getTextSubstring(context.getIndex(), endPos);
323
                    try {
324
                        context.setCurrency(CurrencyUnit.of(code));
325
                        context.setIndex(endPos);
326
                    } catch (IllegalCurrencyException ex) {
327
                        context.setError();
328
                    }
329
                }
330
            }
331
        },
332
        NUMERIC_3_CODE("${numeric3Code}") {
333
            @Override
334
            public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {
335
                appendable.append(money.getCurrencyUnit().getNumeric3Code());
336
            }
337
338
            @Override
339
            public void parse(MoneyParseContext context) {
340
                var endPos = context.getIndex() + 3;
341
                if (endPos > context.getTextLength()) {
342
                    context.setError();
343
                } else {
344
                    var code = context.getTextSubstring(context.getIndex(), endPos);
345
                    try {
346
                        context.setCurrency(CurrencyUnit.ofNumericCode(code));
347
                        context.setIndex(endPos);
348
                    } catch (IllegalCurrencyException ex) {
349
                        context.setError();
350
                    }
351
                }
352
            }
353
        },
354
        NUMERIC_CODE("${numericCode}") {
355
            @Override
356
            public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {
357
                appendable.append(Integer.toString(money.getCurrencyUnit().getNumericCode()));
358
            }
359
360
            @Override
361
            public void parse(MoneyParseContext context) {
362
                var count = 0;
363
                for (; count < 3 && context.getIndex() + count < context.getTextLength(); count++) {
364
                    var ch = context.getText().charAt(context.getIndex() + count);
365
                    if (ch < '0' || ch > '9') {
366
                        break;
367
                    }
368
                }
369
                var endPos = context.getIndex() + count;
370
                var code = context.getTextSubstring(context.getIndex(), endPos);
371
                try {
372
                    context.setCurrency(CurrencyUnit.ofNumericCode(code));
373
                    context.setIndex(endPos);
374
                } catch (IllegalCurrencyException ex) {
375
                    context.setError();
376
                }
377
            }
378
        };
379
380
        private final String toString;
381
382
        private Singletons(String toString) {
383
            this.toString = toString;
384
        }
385
386
        @Override
387
        public String toString() {
388
            return toString;
389
        }
390
    }
391
392
    //-----------------------------------------------------------------------
393
    /**
394
     * Handles the singleton outputs.
395
     */
396
    private static enum SingletonPrinters implements MoneyPrinter {
397
        LOCALIZED_SYMBOL;
398
399
        @Override
400
        public void print(MoneyPrintContext context, Appendable appendable, BigMoney money) throws IOException {
401
            appendable.append(money.getCurrencyUnit().getSymbol(context.getLocale()));
402
        }
403
404
        @Override
405
        public String toString() {
406
            return "${symbolLocalized}";
407
        }
408
    }
409
410
}
411