This repository has been archived by the owner on Mar 20, 2018. It is now read-only.
/
DateTimeFormatterBuilder.java
2767 lines (2610 loc) · 117 KB
/
DateTimeFormatterBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2008-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package javax.time.format;
import static javax.time.calendrical.ChronoField.INSTANT_SECONDS;
import static javax.time.calendrical.ChronoField.NANO_OF_SECOND;
import static javax.time.calendrical.ChronoField.OFFSET_SECONDS;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.time.DateTimeException;
import javax.time.Instant;
import javax.time.ZoneId;
import javax.time.ZoneOffset;
import javax.time.ZonedDateTime;
import javax.time.calendrical.ChronoField;
import javax.time.calendrical.DateTimeAccessor.Query;
import javax.time.calendrical.DateTimeBuilder;
import javax.time.calendrical.DateTimeField;
import javax.time.calendrical.DateTimeValueRange;
import javax.time.chrono.Chrono;
import javax.time.chrono.ISOChrono;
import javax.time.format.SimpleDateTimeTextProvider.LocaleStore;
import javax.time.jdk8.Jdk8Methods;
import javax.time.zone.ZoneRulesProvider;
/**
* Builder to create date-time formatters.
* <p>
* This allows a {@code DateTimeFormatter} to be created.
* All date-time formatters are created ultimately using this builder.
* <p>
* The basic elements of date-time can all be added:
* <p><ul>
* <li>Value - a numeric value</li>
* <li>Fraction - a fractional value including the decimal place. Always use this when
* outputting fractions to ensure that the fraction is parsed correctly</li>
* <li>Text - the textual equivalent for the value</li>
* <li>OffsetId/Offset - the {@link ZoneOffset zone offset}</li>
* <li>ZoneId - the {@link ZoneId time-zone} id</li>
* <li>ZoneText - the name of the time-zone</li>
* <li>Literal - a text literal</li>
* <li>Nested and Optional - formats can be nested or made optional</li>
* <li>Other - the printer and parser interfaces can be used to add user supplied formatting</li>
* </ul><p>
* In addition, any of the elements may be decorated by padding, either with spaces or any other character.
* <p>
* Finally, a shorthand pattern, mostly compatible with {@code java.text.SimpleDateFormat SimpleDateFormat}
* can be used, see {@link #appendPattern(String)}.
* In practice, this simply parses the pattern and calls other methods on the builder.
*
* <h4>Implementation notes</h4>
* This class is a mutable builder intended for use from a single thread.
*/
public final class DateTimeFormatterBuilder {
/**
* The currently active builder, used by the outermost builder.
*/
private DateTimeFormatterBuilder active = this;
/**
* The parent builder, null for the outermost builder.
*/
private final DateTimeFormatterBuilder parent;
/**
* The list of printers that will be used.
*/
private final List<DateTimePrinterParser> printerParsers = new ArrayList<>();
/**
* Whether this builder produces an optional formatter.
*/
private final boolean optional;
/**
* The width to pad the next field to.
*/
private int padNextWidth;
/**
* The character to pad the next field with.
*/
private char padNextChar;
/**
* The index of the last variable width value parser.
*/
private int valueParserIndex = -1;
/**
* Constructs a new instance of the builder.
*/
public DateTimeFormatterBuilder() {
super();
parent = null;
optional = false;
}
/**
* Constructs a new instance of the builder.
*
* @param parent the parent builder, not null
* @param optional whether the formatter is optional, not null
*/
private DateTimeFormatterBuilder(DateTimeFormatterBuilder parent, boolean optional) {
super();
this.parent = parent;
this.optional = optional;
}
//-----------------------------------------------------------------------
/**
* Changes the parse style to be case sensitive for the remainder of the formatter.
* <p>
* Parsing can be case sensitive or insensitive - by default it is case sensitive.
* This controls how text is compared.
* <p>
* When used, this method changes the parsing to be case sensitive from this point onwards.
* As case sensitive is the default, this is normally only needed after calling {@link #parseCaseInsensitive()}.
* The change will remain in force until the end of the formatter that is eventually
* constructed or until {@code parseCaseInsensitive} is called.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder parseCaseSensitive() {
appendInternal(SettingsParser.SENSITIVE);
return this;
}
/**
* Changes the parse style to be case insensitive for the remainder of the formatter.
* <p>
* Parsing can be case sensitive or insensitive - by default it is case sensitive.
* This controls how text is compared.
* <p>
* When used, this method changes the parsing to be case insensitive from this point onwards.
* The change will remain in force until the end of the formatter that is eventually
* constructed or until {@code parseCaseSensitive} is called.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder parseCaseInsensitive() {
appendInternal(SettingsParser.INSENSITIVE);
return this;
}
//-----------------------------------------------------------------------
/**
* Changes the parse style to be strict for the remainder of the formatter.
* <p>
* Parsing can be strict or lenient - by default its strict.
* This controls the degree of flexibility in matching the text and sign styles.
* <p>
* When used, this method changes the parsing to be strict from this point onwards.
* As strict is the default, this is normally only needed after calling {@link #parseLenient()}.
* The change will remain in force until the end of the formatter that is eventually
* constructed or until {@code parseLenient} is called.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder parseStrict() {
appendInternal(SettingsParser.STRICT);
return this;
}
/**
* Changes the parse style to be lenient for the remainder of the formatter.
* Note that case sensitivity is set separately to this method.
* <p>
* Parsing can be strict or lenient - by default its strict.
* This controls the degree of flexibility in matching the text and sign styles.
* Applications calling this method should typically also call {@link #parseCaseInsensitive()}.
* <p>
* When used, this method changes the parsing to be strict from this point onwards.
* The change will remain in force until the end of the formatter that is eventually
* constructed or until {@code parseStrict} is called.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder parseLenient() {
appendInternal(SettingsParser.LENIENT);
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the value of a date-time field to the formatter using a normal
* output style.
* <p>
* The value of the field will be output during a print.
* If the value cannot be obtained then an exception will be thrown.
* <p>
* The value will be printed as per the normal print of an integer value.
* Only negative numbers will be signed. No padding will be added.
* <p>
* The parser for a variable width value such as this normally behaves greedily, accepting as many
* digits as possible. This behavior can be affected by 'adjacent value parsing'.
* See {@link #appendValue(DateTimeField, int)} for full details.
*
* @param field the field to append, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendValue(DateTimeField field) {
Objects.requireNonNull(field, "field");
active.valueParserIndex = appendInternal(new NumberPrinterParser(field, 1, 19, SignStyle.NORMAL));
return this;
}
/**
* Appends the value of a date-time field to the formatter using a fixed
* width, zero-padded approach.
* <p>
* The value of the field will be output during a print.
* If the value cannot be obtained then an exception will be thrown.
* <p>
* The value will be zero-padded on the left. If the size of the value
* means that it cannot be printed within the width then an exception is thrown.
* If the value of the field is negative then an exception is thrown during printing.
* <p>
* This method supports a special technique of parsing known as 'adjacent value parsing'.
* This technique solves the problem where a variable length value is followed by one or more
* fixed length values. The standard parser is greedy, and thus it would normally
* steal the digits that are needed by the fixed width value parsers that follow the
* variable width one.
* <p>
* No action is required to initiate 'adjacent value parsing'.
* When a call to {@code appendValue} with a variable width is made, the builder
* enters adjacent value parsing setup mode. If the immediately subsequent method
* call or calls on the same builder are to this method, then the parser will reserve
* space so that the fixed width values can be parsed.
* <p>
* For example, consider {@code builder.appendValue(YEAR).appendValue(MONTH_OF_YEAR, 2);}
* The year is a variable width parse of between 1 and 19 digits.
* The month is a fixed width parse of 2 digits.
* Because these were appended to the same builder immediately after one another,
* the year parser will reserve two digits for the month to parse.
* Thus, the text '201106' will correctly parse to a year of 2011 and a month of 6.
* Without adjacent value parsing, the year would greedily parse all six digits and leave
* nothing for the month.
* <p>
* Adjacent value parsing applies to each set of fixed width not-negative values in the parser
* that immediately follow any kind of variable width value.
* Calling any other append method will end the setup of adjacent value parsing.
* Thus, in the unlikely event that you need to avoid adjacent value parsing behavior,
* simply add the {@code appendValue} to another {@code DateTimeFormatterBuilder}
* and add that to this builder.
* <p>
* If the four-parameter version of {@code appendValue} is called with equal minimum
* and maximum widths and a sign style of not-negative then it delegates to this method.
*
* @param field the field to append, not null
* @param width the width of the printed field, from 1 to 19
* @return this, for chaining, not null
* @throws IllegalArgumentException if the width is invalid
*/
public DateTimeFormatterBuilder appendValue(DateTimeField field, int width) {
Objects.requireNonNull(field, "field");
if (width < 1 || width > 19) {
throw new IllegalArgumentException("The width must be from 1 to 19 inclusive but was " + width);
}
NumberPrinterParser pp = new NumberPrinterParser(field, width, width, SignStyle.NOT_NEGATIVE);
return appendFixedWidth(width, pp);
}
/**
* Appends the value of a date-time field to the formatter providing full
* control over printing.
* <p>
* The value of the field will be output during a print.
* If the value cannot be obtained then an exception will be thrown.
* <p>
* This method provides full control of the numeric formatting, including
* zero-padding and the positive/negative sign.
* <p>
* The parser for a variable width value normally behaves greedily, accepting as many
* digits as possible. This behavior can be affected by 'adjacent value parsing'.
* See {@link #appendValue(DateTimeField, int)} for full details.
*
* @param field the field to append, not null
* @param minWidth the minimum field width of the printed field, from 1 to 19
* @param maxWidth the maximum field width of the printed field, from 1 to 19
* @param signStyle the positive/negative output style, not null
* @return this, for chaining, not null
* @throws IllegalArgumentException if the widths are invalid
*/
public DateTimeFormatterBuilder appendValue(
DateTimeField field, int minWidth, int maxWidth, SignStyle signStyle) {
if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE) {
return appendValue(field, maxWidth);
}
Objects.requireNonNull(field, "field");
Objects.requireNonNull(signStyle, "signStyle");
if (minWidth < 1 || minWidth > 19) {
throw new IllegalArgumentException("The minimum width must be from 1 to 19 inclusive but was " + minWidth);
}
if (maxWidth < 1 || maxWidth > 19) {
throw new IllegalArgumentException("The maximum width must be from 1 to 19 inclusive but was " + maxWidth);
}
if (maxWidth < minWidth) {
throw new IllegalArgumentException("The maximum width must exceed or equal the minimum width but " +
maxWidth + " < " + minWidth);
}
NumberPrinterParser pp = new NumberPrinterParser(field, minWidth, maxWidth, signStyle);
if (minWidth == maxWidth) {
appendInternal(pp);
} else {
active.valueParserIndex = appendInternal(pp);
}
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the reduced value of a date-time field to the formatter.
* <p>
* This is typically used for printing and parsing a two digit year.
* The {@code width} is the printed and parsed width.
* The {@code baseValue} is used during parsing to determine the valid range.
* <p>
* For printing, the width is used to determine the number of characters to print.
* The rightmost characters are output to match the width, left padding with zero.
* <p>
* For parsing, exactly the number of characters specified by the width are parsed.
* This is incomplete information however, so the base value is used to complete the parse.
* The base value is the first valid value in a range of ten to the power of width.
* <p>
* For example, a base value of {@code 1980} and a width of {@code 2} will have
* valid values from {@code 1980} to {@code 2079}.
* During parsing, the text {@code "12"} will result in the value {@code 2012} as that
* is the value within the range where the last two digits are "12".
* <p>
* This is a fixed width parser operating using 'adjacent value parsing'.
* See {@link #appendValue(DateTimeField, int)} for full details.
*
* @param field the field to append, not null
* @param width the width of the printed and parsed field, from 1 to 18
* @param baseValue the base value of the range of valid values
* @return this, for chaining, not null
* @throws IllegalArgumentException if the width or base value is invalid
*/
public DateTimeFormatterBuilder appendValueReduced(
DateTimeField field, int width, int baseValue) {
Objects.requireNonNull(field, "field");
ReducedPrinterParser pp = new ReducedPrinterParser(field, width, baseValue);
appendFixedWidth(width, pp);
return this;
}
/**
* Appends a fixed width printer-parser.
*
* @param width the width
* @param pp the printer-parser, not null
* @return this, for chaining, not null
*/
private DateTimeFormatterBuilder appendFixedWidth(int width, NumberPrinterParser pp) {
if (active.valueParserIndex >= 0) {
NumberPrinterParser basePP = (NumberPrinterParser) active.printerParsers.get(active.valueParserIndex);
basePP = basePP.withSubsequentWidth(width);
int activeValueParser = active.valueParserIndex;
active.printerParsers.set(active.valueParserIndex, basePP);
appendInternal(pp);
active.valueParserIndex = activeValueParser;
} else {
appendInternal(pp);
}
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the fractional value of a date-time field to the formatter.
* <p>
* The fractional value of the field will be output including the
* preceeding decimal point. The preceeding value is not output.
* For example, the second-of-minute value of 15 would be output as {@code .25}.
* <p>
* The width of the printed fraction can be controlled. Setting the
* minimum width to zero will cause no output to be generated.
* The printed fraction will have the minimum width necessary between
* the minimum and maximum widths - trailing zeroes are omitted.
* No rounding occurs due to the maximum width - digits are simply dropped.
* <p>
* When parsing in strict mode, the number of parsed digits must be between
* the minimum and maximum width. When parsing in lenient mode, the number
* of parsed digits must be at least the minimum width, up to 9 digits.
* <p>
* If the value cannot be obtained then an exception will be thrown.
* If the value is negative an exception will be thrown.
* If the field does not have a fixed set of valid values then an
* exception will be thrown.
* If the field value in the date-time to be printed is invalid it
* cannot be printed and an exception will be thrown.
*
* @param field the field to append, not null
* @param minWidth the minimum width of the field excluding the decimal point, from 0 to 9
* @param maxWidth the maximum width of the field excluding the decimal point, from 1 to 9
* @param decimalPoint whether to output the localized decimal point symbol
* @return this, for chaining, not null
* @throws IllegalArgumentException if the field has a variable set of valid values
* @throws IllegalArgumentException if either width is invalid
*/
public DateTimeFormatterBuilder appendFraction(
DateTimeField field, int minWidth, int maxWidth, boolean decimalPoint) {
appendInternal(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the text of a date-time field to the formatter using the full
* text style.
* <p>
* The text of the field will be output during a print.
* The value must be within the valid range of the field.
* If the value cannot be obtained then an exception will be thrown.
* If the field has no textual representation, then the numeric value will be used.
* <p>
* The value will be printed as per the normal print of an integer value.
* Only negative numbers will be signed. No padding will be added.
*
* @param field the field to append, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendText(DateTimeField field) {
return appendText(field, TextStyle.FULL);
}
/**
* Appends the text of a date-time field to the formatter.
* <p>
* The text of the field will be output during a print.
* The value must be within the valid range of the field.
* If the value cannot be obtained then an exception will be thrown.
* If the field has no textual representation, then the numeric value will be used.
* <p>
* The value will be printed as per the normal print of an integer value.
* Only negative numbers will be signed. No padding will be added.
*
* @param field the field to append, not null
* @param textStyle the text style to use, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendText(DateTimeField field, TextStyle textStyle) {
Objects.requireNonNull(field, "field");
Objects.requireNonNull(textStyle, "textStyle");
appendInternal(new TextPrinterParser(field, textStyle, DateTimeFormatters.getTextProvider()));
return this;
}
/**
* Appends the text of a date-time field to the formatter using the specified
* map to supply the text.
* <p>
* The standard text outputting methods use the localized text in the JDK.
* This method allows that text to be specified directly.
* The supplied map is not validated by the builder to ensure that printing or
* parsing is possible, thus an invalid map may throw an error during later use.
* <p>
* Supplying the map of text provides considerable flexibility in printing and parsing.
* For example, a legacy application might require or supply the months of the
* year as "JNY", "FBY", "MCH" etc. These do not match the standard set of text
* for localized month names. Using this method, a map can be created which
* defines the connection between each value and the text:
* <pre>
* Map<Long, String> map = new HashMap<>();
* map.put(1, "JNY");
* map.put(2, "FBY");
* map.put(3, "MCH");
* ...
* builder.appendText(MONTH_OF_YEAR, map);
* </pre>
* <p>
* Other uses might be to output the value with a suffix, such as "1st", "2nd", "3rd",
* or as Roman numerals "I", "II", "III", "IV".
* <p>
* During printing, the value is obtained and checked that it is in the valid range.
* If text is not available for the value then it is output as a number.
* During parsing, the parser will match against the map of text and numeric values.
*
* @param field the field to append, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendText(DateTimeField field, Map<Long, String> textLookup) {
Objects.requireNonNull(field, "field");
Objects.requireNonNull(textLookup, "textLookup");
Map<Long, String> copy = new LinkedHashMap<Long, String>(textLookup);
Map<TextStyle, Map<Long, String>> map = Collections.singletonMap(TextStyle.FULL, copy);
final LocaleStore store = new LocaleStore(map);
DateTimeTextProvider provider = new DateTimeTextProvider() {
@Override
public String getText(DateTimeField field, long value, TextStyle style, Locale locale) {
return store.getText(value, style);
}
@Override
public Iterator<Entry<String, Long>> getTextIterator(DateTimeField field, TextStyle style, Locale locale) {
return store.getTextIterator(style);
}
@Override
public Locale[] getAvailableLocales() {
throw new UnsupportedOperationException();
}
};
appendInternal(new TextPrinterParser(field, TextStyle.FULL, provider));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends an instant using ISO-8601 to the formatter.
* <p>
* Instants have a fixed output format.
* They are converted to a date-time with a zone-offset of UTC and printed
* using the standard ISO-8601 format.
* <p>
* An alternative to this method is to print/parse the instant as a single
* epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendInstant() {
appendInternal(new InstantPrinterParser());
return this;
}
/**
* Appends the zone offset, such as '+01:00', to the formatter.
* <p>
* The zone offset ID will be output during a print.
* If the offset cannot be obtained then an exception will be thrown.
* The format is defined by {@link ZoneOffset#getId()}.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendOffsetId() {
return appendOffset("+HH:MM:ss", "Z");
}
/**
* Appends the zone offset, such as '+01:00', to the formatter.
* <p>
* The zone offset will be output during a print.
* If the offset cannot be obtained then an exception will be thrown.
* <p>
* The output format is controlled by a pattern which must be one of the following:
* <p><ul>
* <li>{@code +HH} - hour only, truncating any minute
* <li>{@code +HHMM} - hour and minute, no colon
* <li>{@code +HH:MM} - hour and minute, with colon
* <li>{@code +HHMMss} - hour and minute, with second if non-zero and no colon
* <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero and colon
* <li>{@code +HHMMSS} - hour, minute and second, no colon
* <li>{@code +HH:MM:SS} - hour, minute and second, with colon
* </ul><p>
* The "no offset" text controls what text is printed when the offset is zero.
* Example values would be 'Z', '+00:00', 'UTC' or 'GMT'.
* Three formats are accepted for parsing UTC - the "no offset" text, and the
* plus and minus versions of zero defined by the pattern.
* <p>
* The include colon parameter controls whether a colon should separate the
* numeric fields or not.
* <p>
* The allow seconds parameter controls whether seconds may be output.
* If false then seconds are never output.
* If true then seconds are only output if non-zero.
*
* @param pattern the pattern to use
* @param noOffsetText the text to use when the offset is zero, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) {
appendInternal(new ZoneOffsetPrinterParser(noOffsetText, pattern));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the time-zone ID, such as 'Europe/Paris' or '+02:00', to the formatter.
* <p>
* A {@link ZoneId} can be either a {@code ZoneOffset} or an ID of a region
* such as 'America/New_York'. This prints and parses both types.
* If the zone cannot be obtained then an exception will be thrown.
*
* @return this, for chaining, not null
* @see #appendZoneRegionId()
*/
public DateTimeFormatterBuilder appendZoneId() {
appendInternal(new ZoneIdPrinterParser(false));
return this;
}
/**
* Appends the time-zone region ID, such as 'Europe/Paris', to the formatter,
* printing nothing if the zone ID is a {@code ZoneOffset}.
* <p>
* A {@link ZoneId} can be either a {@code ZoneOffset} or an ID of a region
* such as 'America/New_York'. This only prints if the ID is a region.
* Both types are parsed, however the parsing of the offset is optional.
* If the zone cannot be obtained then an exception will be thrown.
*
* @return this, for chaining, not null
* @see #appendZoneId()
*/
public DateTimeFormatterBuilder appendZoneRegionId() {
appendInternal(new ZoneIdPrinterParser(true));
return this;
}
/**
* Appends the time-zone name, such as 'British Summer Time', to the formatter.
* <p>
* The time-zone name will be output during a print.
* If the zone cannot be obtained then an exception will be thrown.
* <p>
* The zone name is obtained from the formatting symbols.
* Different names may be output depending on whether daylight saving time applies.
* <p>
* If the date, time or offset cannot be obtained it may not be possible to
* determine which text to output. In this case, the text representing time
* without daylight savings (winter time) will be used.
*
* @param textStyle the text style to use, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle) {
appendInternal(new ZoneTextPrinterParser(textStyle));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the chronology ID to the formatter.
* <p>
* The chronology ID will be output during a print.
* If the chronology cannot be obtained then an exception will be thrown.
*
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendChronoId() {
appendInternal(new ChronoPrinterParser(null));
return this;
}
/**
* Appends the chronology name to the formatter.
* <p>
* The calendar system name will be output during a print.
* If the chronology cannot be obtained then an exception will be thrown.
* The calendar system name is obtained from the formatting symbols.
*
* @param textStyle the text style to use, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendChronoText(TextStyle textStyle) {
Objects.requireNonNull(textStyle, "textStyle");
appendInternal(new ChronoPrinterParser(textStyle));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends a localized date-time pattern to the formatter.
* <p>
* The pattern is resolved lazily using the locale being used during the print/parse
* (stored in {@link DateTimeFormatter}.
* <p>
* The pattern can vary by chronology, although typically it doesn't.
* This method uses the standard ISO chronology patterns.
*
* @param dateStyle the date style to use, null means no date required
* @param timeStyle the time style to use, null means no time required
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle) {
return appendLocalized(dateStyle, timeStyle, ISOChrono.INSTANCE);
}
/**
* Appends a localized date-time pattern to the formatter.
* <p>
* The pattern is resolved lazily using the locale being used during the print/parse
* (stored in {@link DateTimeFormatter}.
* <p>
* The pattern can vary by chronology, although typically it doesn't.
* This method allows the chronology to be specified.
*
* @param dateStyle the date style to use, null means no date required
* @param timeStyle the time style to use, null means no time required
* @param chrono the chronology to use, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle, Chrono<?> chrono) {
Objects.requireNonNull(chrono, "chrono");
if (dateStyle != null || timeStyle != null) {
appendInternal(new LocalizedPrinterParser(dateStyle, timeStyle, chrono));
}
return this;
}
//-----------------------------------------------------------------------
/**
* Appends a character literal to the formatter.
* <p>
* This character will be output during a print.
*
* @param literal the literal to append, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendLiteral(char literal) {
appendInternal(new CharLiteralPrinterParser(literal));
return this;
}
/**
* Appends a string literal to the formatter.
* <p>
* This string will be output during a print.
* <p>
* If the literal is empty, nothing is added to the formatter.
*
* @param literal the literal to append, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendLiteral(String literal) {
Objects.requireNonNull(literal, "literal");
if (literal.length() > 0) {
if (literal.length() == 1) {
appendInternal(new CharLiteralPrinterParser(literal.charAt(0)));
} else {
appendInternal(new StringLiteralPrinterParser(literal));
}
}
return this;
}
//-----------------------------------------------------------------------
/**
* Appends all the elements of a formatter to the builder.
* <p>
* This method has the same effect as appending each of the constituent
* parts of the formatter directly to this builder.
*
* @param formatter the formatter to add, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder append(DateTimeFormatter formatter) {
Objects.requireNonNull(formatter, "formatter");
appendInternal(formatter.toPrinterParser(false));
return this;
}
/**
* Appends a formatter to the builder which will optionally print/parse.
* <p>
* This method has the same effect as appending each of the constituent
* parts directly to this builder surrounded by an {@link #optionalStart()} and
* {@link #optionalEnd()}.
* <p>
* The formatter will print if data is available for all the fields contained within it.
* The formatter will parse if the string matches, otherwise no error is returned.
*
* @param formatter the formatter to add, not null
* @return this, for chaining, not null
*/
public DateTimeFormatterBuilder appendOptional(DateTimeFormatter formatter) {
Objects.requireNonNull(formatter, "formatter");
appendInternal(formatter.toPrinterParser(true));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends the elements defined by the specified pattern to the builder.
* <p>
* All letters 'A' to 'Z' and 'a' to 'z' are reserved as pattern letters.
* The following pattern letters are defined:
* <pre>
* Symbol Meaning Presentation Examples
* ------ ------- ------------ -------
* G era number/text 1; 01; AD; Anno Domini
* y year year 2004; 04
* D day-of-year number 189
* M month-of-year number/text 7; 07; Jul; July; J
* d day-of-month number 10
*
* Q quarter-of-year number/text 3; 03; Q3
* Y week-based-year year 1996; 96
* w week-of-week-based-year number 27
* E day-of-week number/text 2; Tue; Tuesday; T
* F week-of-month number 3
*
* a am-pm-of-day text PM
* h clock-hour-of-am-pm (1-12) number 12
* K hour-of-am-pm (0-11) number 0
* k clock-hour-of-am-pm (1-24) number 0
*
* H hour-of-day (0-23) number 0
* m minute-of-hour number 30
* s second-of-minute number 55
* S fraction-of-second fraction 978
* A milli-of-day number 1234
* n nano-of-second number 987654321
* N nano-of-day number 1234000000
*
* I time-zone ID zoneId America/Los_Angeles
* z time-zone name text Pacific Standard Time; PST
* Z zone-offset offset-Z +0000; -0800; -08:00;
* X zone-offset 'Z' for zero offset-X Z; -0800; -08:00;
*
* p pad next pad modifier 1
*
* ' escape for text delimiter
* '' single quote literal '
* [ optional section start
* ] optional section end
* </pre>
* <p>
* The count of pattern letters determine the format.
* <p>
* <b>Text</b>: The text style is determined based on the number of pattern letters used.
* Less than 4 pattern letters will use the {@link TextStyle#SHORT short form}.
* Exactly 4 pattern letters will use the {@link TextStyle#FULL full form}.
* Exactly 5 pattern letters will use the {@link TextStyle#NARROW narrow form}.
* <p>
* <b>Number</b>: If the count of letters is one, then the value is printed using the minimum number
* of digits and without padding as per {@link #appendValue(DateTimeField)}. Otherwise, the
* count of digits is used as the width of the output field as per {@link #appendValue(DateTimeField, int)}.
* <p>
* <b>Number/Text</b>: If the count of pattern letters is 3 or greater, use the Text rules above.
* Otherwise use the Number rules above.
* <p>
* <b>Fraction</b>: Outputs the nano-of-second field as a fraction-of-second.
* The nano-of-second value has nine digits, thus the count of pattern letters is from 1 to 9.
* If it is less than 9, then the nano-of-second value is truncated, with only the most
* significant digits being output.
* When parsing in strict mode, the number of parsed digits must match the count of pattern letters.
* When parsing in lenient mode, the number of parsed digits must be at least the count of pattern
* letters, up to 9 digits.
* <p>
* <b>Year</b>: The count of letters determines the minimum field width below which padding is used.
* If the count of letters is two, then a {@link #appendValueReduced reduced} two digit form is used.
* For printing, this outputs the rightmost two digits. For parsing, this will parse using the
* base value of 2000, resulting in a year within the range 2000 to 2099 inclusive.
* If the count of letters is less than four (but not two), then the sign is only output for negative
* years as per {@link SignStyle#NORMAL}.
* Otherwise, the sign is output if the pad width is exceeded, as per {@link SignStyle#EXCEEDS_PAD}
* <p>
* <b>ZoneId</b>: 'I' outputs the zone ID, such as 'Europe/Paris'.
* <p>
* <b>Offset X</b>: This formats the offset using 'Z' when the offset is zero.
* One letter outputs just the hour', such as '+01'
* Two letters outputs the hour and minute, without a colon, such as '+0130'.
* Three letters outputs the hour and minute, with a colon, such as '+01:30'.
* Four letters outputs the hour and minute and optional second, without a colon, such as '+013015'.
* Five letters outputs the hour and minute and optional second, with a colon, such as '+01:30:15'.
* <p>
* <b>Offset Z</b>: This formats the offset using '+0000' or '+00:00' when the offset is zero.
* One or two letters outputs the hour and minute, without a colon, such as '+0130'.
* Three letters outputs the hour and minute, with a colon, such as '+01:30'.
* <p>
* <b>Zone names</b>: Time zone names ('z') cannot be parsed.
* <p>
* <b>Optional section</b>: The optional section markers work exactly like calling {@link #optionalStart()}
* and {@link #optionalEnd()}.
* <p>
* <b>Pad modifier</b>: Modifies the pattern that immediately follows to be padded with spaces.
* The pad width is determined by the number of pattern letters.
* This is the same as calling {@link #padNext(int)}.
* <p>
* For example, 'ppH' outputs the hour-of-day padded on the left with spaces to a width of 2.
* <p>
* Any unrecognized letter is an error.
* Any non-letter character, other than '[', ']' and the single quote will be output directly.
* Despite this, it is recommended to use single quotes around all characters that you want to
* output directly to ensure that future changes do not break your application.
* <p>
* The pattern string is similar, but not identical, to {@link java.text.SimpleDateFormat SimpleDateFormat}.
* Pattern letters 'E' and 'u' are merged, which changes the meaning of "E" and "EE" to be numeric.
* Pattern letter 'W' is not available.
* Pattern letters 'Z' and 'X' are extended.
* Pattern letter 'y' and 'Y' parse years of two digits and more than 4 digits differently.
* Pattern letters 'Q', 'n', 'A', 'N', 'I', 'f' and 'p' are added.
* Number types will reject large numbers.
* The pattern string is also similar, but not identical, to that defined by the
* Unicode Common Locale Data Repository (CLDR).
*
* @param pattern the pattern to add, not null
* @return this, for chaining, not null
* @throws IllegalArgumentException if the pattern is invalid
*/
public DateTimeFormatterBuilder appendPattern(String pattern) {
Objects.requireNonNull(pattern, "pattern");
parsePattern(pattern);
return this;
}
private void parsePattern(String pattern) {
for (int pos = 0; pos < pattern.length(); pos++) {
char cur = pattern.charAt(pos);
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
int start = pos++;
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
int count = pos - start;
// padding
if (cur == 'p') {
int pad = 0;
if (pos < pattern.length()) {
cur = pattern.charAt(pos);
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
pad = count;
start = pos++;
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
count = pos - start;
}
}
if (pad == 0) {
throw new IllegalArgumentException(
"Pad letter 'p' must be followed by valid pad pattern: " + pattern);
}
padNext(pad); // pad and continue parsing
}
// main rules
DateTimeField field = FIELD_MAP.get(cur);
if (field != null) {
parseField(cur, count, field);
} else if (cur == 'z') {
if (count < 4) {
appendZoneText(TextStyle.SHORT);
} else {
appendZoneText(TextStyle.FULL);
}
} else if (cur == 'I') {
appendZoneId();
} else if (cur == 'Z') {
if (count > 3) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
if (count < 3) {
appendOffset("+HHMM", "+0000");
} else {
appendOffset("+HH:MM", "+00:00");
}
} else if (cur == 'X') {
if (count > 5) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendOffset(ZoneOffsetPrinterParser.PATTERNS[count - 1], "Z");
} else {
throw new IllegalArgumentException("Unknown pattern letter: " + cur);
}
pos--;
} else if (cur == '\'') {
// parse literals
int start = pos++;
for ( ; pos < pattern.length(); pos++) {
if (pattern.charAt(pos) == '\'') {
if (pos + 1 < pattern.length() && pattern.charAt(pos + 1) == '\'') {