1
2
3
4
5
6 package dev.metaschema.core.metapath.function.library;
7
8 import java.time.DayOfWeek;
9 import java.time.LocalDate;
10 import java.time.ZoneOffset;
11 import java.time.temporal.IsoFields;
12 import java.time.temporal.WeekFields;
13 import java.util.ArrayList;
14 import java.util.Collections;
15 import java.util.List;
16 import java.util.Locale;
17 import java.util.Set;
18
19 import dev.metaschema.core.metapath.function.FormatDateTimeFunctionException;
20 import dev.metaschema.core.metapath.item.atomic.IIntegerItem;
21 import dev.metaschema.core.metapath.item.atomic.ITemporalItem;
22 import edu.umd.cs.findbugs.annotations.NonNull;
23 import edu.umd.cs.findbugs.annotations.Nullable;
24
25
26
27
28
29
30
31 public final class DateTimeFormatUtil {
32
33
34
35
36 private static final Set<Character> VALID_SPECIFIERS = Set.of(
37 'Y', 'M', 'D', 'd', 'F', 'W', 'w', 'H', 'h', 'P', 'm', 's', 'f', 'Z',
38 'z', 'C', 'E');
39
40
41
42
43
44 private static final Set<Character> SECOND_MODIFIERS = Set.of('a', 't', 'c', 'o');
45
46 private DateTimeFormatUtil() {
47
48 }
49
50
51
52
53 public static class FormatComponent {
54
55
56
57 protected FormatComponent() {
58
59 }
60 }
61
62
63
64
65 public static class LiteralComponent
66 extends FormatComponent {
67 @NonNull
68 private final String text;
69
70
71
72
73
74
75
76 public LiteralComponent(@NonNull String text) {
77 this.text = text;
78 }
79
80
81
82
83
84
85 @NonNull
86 public String getText() {
87 return text;
88 }
89 }
90
91
92
93
94
95 public static class VariableMarkerComponent
96 extends FormatComponent {
97 private final char specifier;
98 @Nullable
99 private final String primaryModifier;
100 @Nullable
101 private final Character secondModifier;
102 @Nullable
103 private final Integer minWidth;
104 @Nullable
105 private final Integer maxWidth;
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 public VariableMarkerComponent(
122 char specifier,
123 @Nullable String primaryModifier,
124 @Nullable Character secondModifier,
125 @Nullable Integer minWidth,
126 @Nullable Integer maxWidth) {
127 this.specifier = specifier;
128 this.primaryModifier = primaryModifier;
129 this.secondModifier = secondModifier;
130 this.minWidth = minWidth;
131 this.maxWidth = maxWidth;
132 }
133
134
135
136
137
138
139 public char getSpecifier() {
140 return specifier;
141 }
142
143
144
145
146
147
148 @Nullable
149 public String getPrimaryModifier() {
150 return primaryModifier;
151 }
152
153
154
155
156
157
158 @Nullable
159 public Character getSecondModifier() {
160 return secondModifier;
161 }
162
163
164
165
166
167
168 @Nullable
169 public Integer getMinWidth() {
170 return minWidth;
171 }
172
173
174
175
176
177
178 @Nullable
179 public Integer getMaxWidth() {
180 return maxWidth;
181 }
182 }
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201 @NonNull
202 public static List<FormatComponent> parsePictureString(@NonNull String picture) {
203 List<FormatComponent> components = new ArrayList<>();
204 StringBuilder literal = new StringBuilder();
205 int length = picture.length();
206 int index = 0;
207
208 while (index < length) {
209 char ch = picture.charAt(index);
210
211 if (ch == '[') {
212
213 if (index + 1 < length && picture.charAt(index + 1) == '[') {
214 literal.append('[');
215 index += 2;
216 } else {
217
218 if (literal.length() > 0) {
219 components.add(new LiteralComponent(literal.toString()));
220 literal.setLength(0);
221 }
222
223
224 int closeIndex = picture.indexOf(']', index + 1);
225 if (closeIndex < 0) {
226 throw new FormatDateTimeFunctionException(
227 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
228 "Unmatched '[' in picture string: " + picture);
229 }
230
231 String markerContent = picture.substring(index + 1, closeIndex);
232 components.add(parseVariableMarker(markerContent, picture));
233 index = closeIndex + 1;
234 }
235 } else if (ch == ']') {
236
237 if (index + 1 < length && picture.charAt(index + 1) == ']') {
238 literal.append(']');
239 index += 2;
240 } else {
241 throw new FormatDateTimeFunctionException(
242 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
243 "Unmatched ']' in picture string: " + picture);
244 }
245 } else {
246 literal.append(ch);
247 index++;
248 }
249 }
250
251
252 if (literal.length() > 0) {
253 components.add(new LiteralComponent(literal.toString()));
254 }
255
256 return Collections.unmodifiableList(components);
257 }
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272 @NonNull
273 private static VariableMarkerComponent parseVariableMarker(
274 @NonNull String content,
275 @NonNull String picture) {
276
277 String stripped = content.replaceAll("\\s", "");
278
279 if (stripped.isEmpty()) {
280 throw new FormatDateTimeFunctionException(
281 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
282 "Empty variable marker in picture string: " + picture);
283 }
284
285
286 char specifier = stripped.charAt(0);
287 if (!VALID_SPECIFIERS.contains(specifier)) {
288 throw new FormatDateTimeFunctionException(
289 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
290 "Invalid component specifier '" + specifier
291 + "' in picture string: " + picture);
292 }
293
294
295 String remaining = stripped.substring(1);
296
297
298 String presentationPart;
299 String widthPart;
300 int lastComma = remaining.lastIndexOf(',');
301 if (lastComma >= 0) {
302 presentationPart = remaining.substring(0, lastComma);
303 widthPart = remaining.substring(lastComma + 1);
304 } else {
305 presentationPart = remaining;
306 widthPart = null;
307 }
308
309
310 String primaryModifier = null;
311 Character secondModifier = null;
312
313 if (!presentationPart.isEmpty()) {
314 if (presentationPart.length() == 1) {
315
316 primaryModifier = presentationPart;
317 } else {
318
319 char lastChar = presentationPart.charAt(presentationPart.length() - 1);
320 if (SECOND_MODIFIERS.contains(lastChar)) {
321 secondModifier = lastChar;
322 String primary = presentationPart.substring(0, presentationPart.length() - 1);
323 primaryModifier = primary.isEmpty() ? null : primary;
324 } else {
325 primaryModifier = presentationPart;
326 }
327 }
328 }
329
330
331 Integer minWidth = null;
332 Integer maxWidth = null;
333
334 if (widthPart != null) {
335 Integer[] widths = new Integer[2];
336 parseWidth(widthPart, picture, widths);
337 minWidth = widths[0];
338 maxWidth = widths[1];
339 }
340
341 return new VariableMarkerComponent(specifier, primaryModifier, secondModifier,
342 minWidth, maxWidth);
343 }
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360 private static void parseWidth(
361 @NonNull String widthPart,
362 @NonNull String picture,
363 @NonNull Integer[] result) {
364 int dashIndex = widthPart.indexOf('-');
365 String minStr;
366 String maxStr;
367
368 if (dashIndex >= 0) {
369 minStr = widthPart.substring(0, dashIndex);
370 maxStr = widthPart.substring(dashIndex + 1);
371 } else {
372 minStr = widthPart;
373 maxStr = null;
374 }
375
376 Integer minWidth = parseWidthValue(minStr, picture);
377 Integer maxWidth = maxStr != null ? parseWidthValue(maxStr, picture) : null;
378
379
380 if (minWidth != null && minWidth < 1) {
381 throw new FormatDateTimeFunctionException(
382 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
383 "Minimum width must be at least 1 in picture string: " + picture);
384 }
385
386
387 if (minWidth != null && maxWidth != null && maxWidth < minWidth) {
388 throw new FormatDateTimeFunctionException(
389 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
390 "Maximum width must not be less than minimum width in picture string: "
391 + picture);
392 }
393
394 result[0] = minWidth;
395 result[1] = maxWidth;
396 }
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411 @Nullable
412 private static Integer parseWidthValue(@NonNull String value, @NonNull String picture) {
413 if ("*".equals(value)) {
414 return null;
415 }
416 try {
417 return Integer.parseInt(value);
418 } catch (NumberFormatException ex) {
419 throw new FormatDateTimeFunctionException(
420 FormatDateTimeFunctionException.INVALID_PICTURE_STRING,
421 "Invalid width value '" + value + "' in picture string: " + picture,
422 ex);
423 }
424 }
425
426
427
428
429
430
431
432
433 private static final String[] MONTH_NAMES = {
434 "January", "February", "March", "April", "May", "June",
435 "July", "August", "September", "October", "November", "December"
436 };
437
438
439
440
441
442 private static final String[] DAY_NAMES = {
443 "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
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 @NonNull
473 public static String formatDateTime(
474 @NonNull ITemporalItem value,
475 @NonNull String picture,
476 @Nullable String language,
477 @Nullable String calendar,
478 @Nullable String place,
479 @NonNull Set<Character> allowedMarkers) {
480 List<FormatComponent> components = parsePictureString(picture);
481 StringBuilder result = new StringBuilder();
482
483 for (FormatComponent component : components) {
484 if (component instanceof LiteralComponent) {
485 result.append(((LiteralComponent) component).getText());
486 } else {
487 VariableMarkerComponent marker = (VariableMarkerComponent) component;
488 char specifier = marker.getSpecifier();
489
490 if (!allowedMarkers.contains(specifier)) {
491 throw new FormatDateTimeFunctionException(
492 FormatDateTimeFunctionException.COMPONENT_NOT_AVAILABLE,
493 "Component specifier '" + specifier
494 + "' is not available for this type in picture string: " + picture);
495 }
496
497 result.append(formatComponent(value, marker, language));
498 }
499 }
500
501 return result.toString();
502 }
503
504
505
506
507
508
509
510
511
512
513
514
515 @NonNull
516 private static String formatComponent(
517 @NonNull ITemporalItem value,
518 @NonNull VariableMarkerComponent marker,
519 @Nullable String language) {
520 char specifier = marker.getSpecifier();
521 String primaryMod = marker.getPrimaryModifier();
522 Character secondMod = marker.getSecondModifier();
523 Integer minWidth = marker.getMinWidth();
524 Integer maxWidth = marker.getMaxWidth();
525
526 switch (specifier) {
527 case 'Y':
528 return formatYear(value, primaryMod, secondMod, minWidth, maxWidth, language);
529 case 'M':
530 return formatNameableComponent(value.getMonth(), MONTH_NAMES, 1,
531 primaryMod, secondMod, minWidth, maxWidth, language, "1");
532 case 'D':
533 return formatIntegerComponent(value.getDay(),
534 primaryMod, secondMod, minWidth, maxWidth, language, "1");
535 case 'd':
536 return formatDayOfYear(value, primaryMod, secondMod, minWidth, maxWidth, language);
537 case 'F':
538 return formatDayOfWeek(value, primaryMod, secondMod, minWidth, maxWidth, language);
539 case 'W':
540 return formatWeekOfYear(value, primaryMod, secondMod, minWidth, maxWidth, language);
541 case 'w':
542 return formatWeekOfMonth(value, primaryMod, secondMod, minWidth, maxWidth, language);
543 case 'H':
544 return formatIntegerComponent(value.getHour(),
545 primaryMod, secondMod, minWidth, maxWidth, language, "1");
546 case 'h':
547 return formatIntegerComponent(hourIn12(value.getHour()),
548 primaryMod, secondMod, minWidth, maxWidth, language, "1");
549 case 'P':
550 return formatAmPm(value.getHour(), primaryMod, secondMod, minWidth, maxWidth);
551 case 'm':
552 return formatIntegerComponent(value.getMinute(),
553 primaryMod, secondMod, minWidth, maxWidth, language, "01");
554 case 's':
555 return formatIntegerComponent(value.getSecond(),
556 primaryMod, secondMod, minWidth, maxWidth, language, "01");
557 case 'f':
558 return formatFractionalSeconds(value.getNano(), primaryMod, minWidth, maxWidth);
559 case 'Z':
560 return formatTimezone(value, primaryMod, secondMod);
561 case 'z':
562 return formatGmtTimezone(value, primaryMod, secondMod);
563 case 'C':
564 return formatCalendar(primaryMod, minWidth, maxWidth);
565 case 'E':
566 return formatEra(value.getYear(), primaryMod, minWidth, maxWidth);
567 default:
568
569 return "";
570 }
571 }
572
573
574
575
576
577
578
579
580 private static int hourIn12(int hour24) {
581 int h = hour24 % 12;
582 return h == 0 ? 12 : h;
583 }
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602 @NonNull
603 private static String formatYear(
604 @NonNull ITemporalItem value,
605 @Nullable String primaryMod,
606 @Nullable Character secondMod,
607 @Nullable Integer minWidth,
608 @Nullable Integer maxWidth,
609 @Nullable String language) {
610
611 int year = (int) Math.min(Math.abs((long) value.getYear()), Integer.MAX_VALUE);
612
613
614 String effectiveToken = primaryMod != null ? primaryMod : "1";
615
616
617
618
619
620
621 int moduloN = Integer.MAX_VALUE;
622
623 if (maxWidth != null) {
624 moduloN = maxWidth;
625 } else {
626 int mandatoryDigits = countMandatoryDigits(effectiveToken);
627 if (mandatoryDigits >= 2 && isDecimalDigitPattern(effectiveToken)) {
628 moduloN = mandatoryDigits;
629 }
630 }
631
632
633 int displayYear = year;
634 if (moduloN < Integer.MAX_VALUE) {
635 int divisor = (int) Math.pow(10, moduloN);
636 displayYear = year % divisor;
637 }
638
639
640 String formatted = formatIntegerValue(displayYear, effectiveToken, secondMod, language);
641
642
643 formatted = applyWidthModifiers(formatted, minWidth, maxWidth, false);
644
645
646 if (value.getYear() < 0) {
647 formatted = "-" + formatted;
648 }
649
650 return formatted;
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 @NonNull
679 private static String formatNameableComponent(
680 int componentValue,
681 @NonNull String[] names,
682 int nameOffset,
683 @Nullable String primaryMod,
684 @Nullable Character secondMod,
685 @Nullable Integer minWidth,
686 @Nullable Integer maxWidth,
687 @Nullable String language,
688 @NonNull String defaultToken) {
689 String effective = primaryMod != null ? primaryMod : defaultToken;
690
691
692 if (isNameFormat(effective)) {
693 String name = names[componentValue - nameOffset];
694 name = applyNameCase(name, effective);
695 return applyWidthModifiers(name, minWidth, maxWidth, true);
696 }
697
698 return formatIntegerComponent(componentValue, primaryMod, secondMod,
699 minWidth, maxWidth, language, defaultToken);
700 }
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721 @NonNull
722 private static String formatIntegerComponent(
723 int componentValue,
724 @Nullable String primaryMod,
725 @Nullable Character secondMod,
726 @Nullable Integer minWidth,
727 @Nullable Integer maxWidth,
728 @Nullable String language,
729 @NonNull String defaultToken) {
730 String effectiveToken = primaryMod != null ? primaryMod : defaultToken;
731
732 String formatted = formatIntegerValue(componentValue, effectiveToken, secondMod, language);
733 return applyWidthModifiers(formatted, minWidth, maxWidth, false);
734 }
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752 @NonNull
753 private static String formatIntegerValue(
754 int componentValue,
755 @NonNull String formatToken,
756 @Nullable Character secondMod,
757 @Nullable String language) {
758
759 String picture = formatToken;
760 if (secondMod != null && secondMod == 'o') {
761 picture = picture + ";o";
762 }
763
764 return FnFormatInteger.fnFormatInteger(
765 IIntegerItem.valueOf(componentValue),
766 picture,
767 language);
768 }
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787 @NonNull
788 private static String formatDayOfYear(
789 @NonNull ITemporalItem value,
790 @Nullable String primaryMod,
791 @Nullable Character secondMod,
792 @Nullable Integer minWidth,
793 @Nullable Integer maxWidth,
794 @Nullable String language) {
795
796 int proxyYear = Math.max(1, value.getYear());
797 int dayOfYear = LocalDate.of(proxyYear, value.getMonth(), value.getDay()).getDayOfYear();
798 return formatIntegerComponent(dayOfYear, primaryMod, secondMod, minWidth, maxWidth, language, "1");
799 }
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819 @NonNull
820 private static String formatDayOfWeek(
821 @NonNull ITemporalItem value,
822 @Nullable String primaryMod,
823 @Nullable Character secondMod,
824 @Nullable Integer minWidth,
825 @Nullable Integer maxWidth,
826 @Nullable String language) {
827
828 int proxyYear = Math.max(1, value.getYear());
829 DayOfWeek dow = LocalDate.of(proxyYear, value.getMonth(), value.getDay()).getDayOfWeek();
830 int isoValue = dow.getValue();
831
832
833 String effective = primaryMod != null ? primaryMod : "n";
834
835 if (isNameFormat(effective)) {
836 String name = DAY_NAMES[isoValue - 1];
837 name = applyNameCase(name, effective);
838 return applyWidthModifiers(name, minWidth, maxWidth, true);
839 }
840
841 return formatIntegerComponent(isoValue, primaryMod, secondMod,
842 minWidth, maxWidth, language, "n");
843 }
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862 @NonNull
863 private static String formatWeekOfYear(
864 @NonNull ITemporalItem value,
865 @Nullable String primaryMod,
866 @Nullable Character secondMod,
867 @Nullable Integer minWidth,
868 @Nullable Integer maxWidth,
869 @Nullable String language) {
870
871 int proxyYear = Math.max(1, value.getYear());
872 int week = LocalDate.of(proxyYear, value.getMonth(), value.getDay())
873 .get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
874 return formatIntegerComponent(week, primaryMod, secondMod, minWidth, maxWidth, language, "1");
875 }
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894 @NonNull
895 private static String formatWeekOfMonth(
896 @NonNull ITemporalItem value,
897 @Nullable String primaryMod,
898 @Nullable Character secondMod,
899 @Nullable Integer minWidth,
900 @Nullable Integer maxWidth,
901 @Nullable String language) {
902
903 int proxyYear = Math.max(1, value.getYear());
904 int week = LocalDate.of(proxyYear, value.getMonth(), value.getDay())
905 .get(WeekFields.ISO.weekOfMonth());
906 return formatIntegerComponent(week, primaryMod, secondMod, minWidth, maxWidth, language, "1");
907 }
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924 @NonNull
925 private static String formatAmPm(
926 int hour,
927 @Nullable String primaryMod,
928 @Nullable Character secondMod,
929 @Nullable Integer minWidth,
930 @Nullable Integer maxWidth) {
931 String effective = primaryMod != null ? primaryMod : "n";
932 String base = hour < 12 ? "am" : "pm";
933
934 String result;
935 if ("N".equals(effective)) {
936 result = base.toUpperCase(Locale.ROOT);
937 } else if ("Nn".equals(effective)) {
938 result = Character.toUpperCase(base.charAt(0)) + base.substring(1);
939 } else {
940
941 result = base;
942 }
943
944 return applyWidthModifiers(result, minWidth, maxWidth, true);
945 }
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965 @NonNull
966 private static String formatFractionalSeconds(
967 int nano,
968 @Nullable String primaryModifier,
969 @Nullable Integer minWidth,
970 @Nullable Integer maxWidth) {
971
972 String nanoStr = String.format("%09d", nano);
973 String effective = primaryModifier != null ? primaryModifier : "1";
974 int mandatoryDigits = countMandatoryDigits(effective);
975
976 String result;
977 if (mandatoryDigits <= 1 && effective.length() <= 1) {
978
979 result = nanoStr.replaceAll("0+$", "");
980 if (result.isEmpty()) {
981 result = "0";
982 }
983
984
985 if (maxWidth != null && result.length() > maxWidth) {
986 result = result.substring(0, maxWidth);
987 }
988 if (minWidth != null && result.length() < minWidth) {
989 result = result + "0".repeat(minWidth - result.length());
990 }
991 } else {
992
993 int numDigits = mandatoryDigits;
994 if (minWidth != null && minWidth > numDigits) {
995 numDigits = minWidth;
996 }
997 if (maxWidth != null && maxWidth < numDigits) {
998 numDigits = maxWidth;
999 }
1000 result = nanoStr.substring(0, Math.min(numDigits, 9));
1001 }
1002
1003 return result;
1004 }
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017 @NonNull
1018 private static String formatTimezone(
1019 @NonNull ITemporalItem value,
1020 @Nullable String primaryMod,
1021 @Nullable Character secondMod) {
1022 ZoneOffset offset = value.getZoneOffset();
1023
1024
1025 if (primaryMod != null && "Z".equals(primaryMod)) {
1026 return formatMilitaryTimezone(offset);
1027 }
1028
1029 if (offset == null) {
1030 return "";
1031 }
1032
1033
1034 boolean useZ = secondMod != null && secondMod == 't';
1035 if (useZ && offset.getTotalSeconds() == 0) {
1036 return "Z";
1037 }
1038
1039 String effective = primaryMod != null ? primaryMod : "01:01";
1040 return formatTimezoneNumeric(offset, effective);
1041 }
1042
1043
1044
1045
1046
1047
1048
1049
1050 @NonNull
1051 private static String formatMilitaryTimezone(@Nullable ZoneOffset offset) {
1052 if (offset == null) {
1053 return "J";
1054 }
1055
1056 int totalSeconds = offset.getTotalSeconds();
1057 int totalMinutes = totalSeconds / 60;
1058 int hours = totalMinutes / 60;
1059 int minutes = totalMinutes % 60;
1060
1061 if (totalSeconds == 0) {
1062 return "Z";
1063 }
1064
1065
1066 if (minutes == 0 && hours >= -12 && hours <= 12) {
1067 if (hours > 0) {
1068
1069 if (hours <= 9) {
1070 return String.valueOf((char) ('A' + hours - 1));
1071 }
1072
1073 return String.valueOf((char) ('A' + hours));
1074 }
1075
1076 return String.valueOf((char) ('N' + (-hours) - 1));
1077 }
1078
1079
1080 return formatTimezoneNumeric(offset, "01:01");
1081 }
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108 @NonNull
1109 private static String formatTimezoneNumeric(
1110 @NonNull ZoneOffset offset,
1111 @NonNull String pattern) {
1112 int totalSeconds = offset.getTotalSeconds();
1113 String sign = totalSeconds >= 0 ? "+" : "-";
1114 int absSeconds = Math.abs(totalSeconds);
1115 int hours = absSeconds / 3600;
1116 int minutes = (absSeconds % 3600) / 60;
1117
1118
1119 boolean hasSeparator = pattern.contains(":") || pattern.contains(".");
1120 char separator = pattern.contains(":") ? ':' : '.';
1121 String digitsPart = pattern.replace(":", "").replace(".", "");
1122 int digitCount = digitsPart.length();
1123
1124 boolean padHours;
1125 boolean alwaysShowMinutes;
1126
1127 if (hasSeparator) {
1128
1129 int sepIndex = pattern.indexOf(separator);
1130 padHours = sepIndex >= 2;
1131 alwaysShowMinutes = true;
1132 } else if (digitCount >= 3) {
1133
1134 padHours = digitCount >= 4;
1135 alwaysShowMinutes = true;
1136
1137 } else {
1138
1139 padHours = digitCount >= 2;
1140 alwaysShowMinutes = false;
1141 }
1142
1143 String hoursStr = padHours
1144 ? String.format("%02d", hours)
1145 : String.valueOf(hours);
1146
1147 if (alwaysShowMinutes) {
1148 String minutesStr = String.format("%02d", minutes);
1149 if (hasSeparator) {
1150 return sign + hoursStr + separator + minutesStr;
1151 }
1152 return sign + hoursStr + minutesStr;
1153 }
1154
1155
1156 if (minutes != 0) {
1157 String minutesStr = String.format("%02d", minutes);
1158 return sign + hoursStr + ":" + minutesStr;
1159 }
1160
1161 return sign + hoursStr;
1162 }
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175 @NonNull
1176 private static String formatGmtTimezone(
1177 @NonNull ITemporalItem value,
1178 @Nullable String primaryMod,
1179 @Nullable Character secondMod) {
1180 ZoneOffset offset = value.getZoneOffset();
1181 if (offset == null) {
1182 return "";
1183 }
1184
1185 String tzPart = formatTimezoneNumeric(offset, primaryMod != null ? primaryMod : "01:01");
1186 return "GMT" + tzPart;
1187 }
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200 @NonNull
1201 private static String formatCalendar(
1202 @Nullable String primaryMod,
1203 @Nullable Integer minWidth,
1204 @Nullable Integer maxWidth) {
1205 String effective = primaryMod != null ? primaryMod : "n";
1206 String result = applyNameCase("ad", effective);
1207 return applyWidthModifiers(result, minWidth, maxWidth, true);
1208 }
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224 @NonNull
1225 private static String formatEra(
1226 int year,
1227 @Nullable String primaryMod,
1228 @Nullable Integer minWidth,
1229 @Nullable Integer maxWidth) {
1230 String effective = primaryMod != null ? primaryMod : "n";
1231 String base = year >= 0 ? "ad" : "bc";
1232 String result = applyNameCase(base, effective);
1233 return applyWidthModifiers(result, minWidth, maxWidth, true);
1234 }
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247 private static boolean isNameFormat(@NonNull String modifier) {
1248 return "N".equals(modifier) || "Nn".equals(modifier) || "n".equals(modifier);
1249 }
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261 @NonNull
1262 private static String applyNameCase(@NonNull String name, @NonNull String modifier) {
1263 switch (modifier) {
1264 case "N":
1265 return name.toUpperCase(Locale.ROOT);
1266 case "n":
1267 return name.toLowerCase(Locale.ROOT);
1268 case "Nn":
1269 if (name.isEmpty()) {
1270 return name;
1271 }
1272 return Character.toUpperCase(name.charAt(0))
1273 + name.substring(1).toLowerCase(Locale.ROOT);
1274 default:
1275 return name;
1276 }
1277 }
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294 @NonNull
1295 private static String applyWidthModifiers(
1296 @NonNull String value,
1297 @Nullable Integer minWidth,
1298 @Nullable Integer maxWidth,
1299 boolean isName) {
1300 String result = value;
1301
1302
1303 if (maxWidth != null && result.length() > maxWidth) {
1304 result = result.substring(0, maxWidth);
1305 }
1306
1307
1308 if (minWidth != null && result.length() < minWidth) {
1309 int padAmount = minWidth - result.length();
1310 if (isName) {
1311
1312 result = result + " ".repeat(padAmount);
1313 } else {
1314
1315 result = "0".repeat(padAmount) + result;
1316 }
1317 }
1318
1319 return result;
1320 }
1321
1322
1323
1324
1325
1326
1327
1328
1329 private static int countMandatoryDigits(@NonNull String pattern) {
1330 int count = 0;
1331 for (int i = 0; i < pattern.length(); i++) {
1332 if (Character.isDigit(pattern.charAt(i))) {
1333 count++;
1334 }
1335 }
1336 return count;
1337 }
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347 private static boolean isDecimalDigitPattern(@NonNull String token) {
1348 if (token.isEmpty()) {
1349 return false;
1350 }
1351 for (int i = 0; i < token.length(); i++) {
1352 char ch = token.charAt(i);
1353 if (!Character.isDigit(ch) && ch != '#' && ch != ',' && ch != '.' && ch != ';') {
1354 return false;
1355 }
1356 }
1357 return true;
1358 }
1359 }