849 * epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}.
850 *
851 * @param fractionalDigits the number of fractional second digits to format with,
852 * from 0 to 9, or -1 to use as many digits as necessary
853 * @return this, for chaining, not null
854 * @throws IllegalArgumentException if the number of fractional digits is invalid
855 */
856 public DateTimeFormatterBuilder appendInstant(int fractionalDigits) {
857 if (fractionalDigits < -1 || fractionalDigits > 9) {
858 throw new IllegalArgumentException("The fractional digits must be from -1 to 9 inclusive but was " + fractionalDigits);
859 }
860 appendInternal(new InstantPrinterParser(fractionalDigits));
861 return this;
862 }
863
864 //-----------------------------------------------------------------------
865 /**
866 * Appends the zone offset, such as '+01:00', to the formatter.
867 * <p>
868 * This appends an instruction to format/parse the offset ID to the builder.
869 * This is equivalent to calling {@code appendOffset("+HH:MM:ss", "Z")}.
870 *
871 * @return this, for chaining, not null
872 */
873 public DateTimeFormatterBuilder appendOffsetId() {
874 appendInternal(OffsetIdPrinterParser.INSTANCE_ID_Z);
875 return this;
876 }
877
878 /**
879 * Appends the zone offset, such as '+01:00', to the formatter.
880 * <p>
881 * This appends an instruction to format/parse the offset ID to the builder.
882 * <p>
883 * During formatting, the offset is obtained using a mechanism equivalent
884 * to querying the temporal with {@link TemporalQueries#offset()}.
885 * It will be printed using the format defined below.
886 * If the offset cannot be obtained then an exception is thrown unless the
887 * section of the formatter is optional.
888 * <p>
889 * During parsing, the offset is parsed using the format defined below.
890 * If the offset cannot be parsed then an exception is thrown unless the
891 * section of the formatter is optional.
892 * <p>
893 * The format of the offset is controlled by a pattern which must be one
894 * of the following:
895 * <ul>
896 * <li>{@code +HH} - hour only, ignoring minute and second
897 * <li>{@code +HHmm} - hour, with minute if non-zero, ignoring second, no colon
898 * <li>{@code +HH:mm} - hour, with minute if non-zero, ignoring second, with colon
899 * <li>{@code +HHMM} - hour and minute, ignoring second, no colon
900 * <li>{@code +HH:MM} - hour and minute, ignoring second, with colon
901 * <li>{@code +HHMMss} - hour and minute, with second if non-zero, no colon
902 * <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon
903 * <li>{@code +HHMMSS} - hour, minute and second, no colon
904 * <li>{@code +HH:MM:SS} - hour, minute and second, with colon
905 * </ul>
906 * The "no offset" text controls what text is printed when the total amount of
907 * the offset fields to be output is zero.
908 * Example values would be 'Z', '+00:00', 'UTC' or 'GMT'.
909 * Three formats are accepted for parsing UTC - the "no offset" text, and the
910 * plus and minus versions of zero defined by the pattern.
911 *
912 * @param pattern the pattern to use, not null
913 * @param noOffsetText the text to use when the offset is zero, not null
914 * @return this, for chaining, not null
915 * @throws IllegalArgumentException if the pattern is invalid
916 */
917 public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) {
918 appendInternal(new OffsetIdPrinterParser(pattern, noOffsetText));
919 return this;
920 }
921
922 /**
923 * Appends the localized zone offset, such as 'GMT+01:00', to the formatter.
924 * <p>
3298 } catch (RuntimeException ex) {
3299 return ~position;
3300 }
3301 int successPos = pos;
3302 successPos = context.setParsedField(INSTANT_SECONDS, instantSecs, position, successPos);
3303 return context.setParsedField(NANO_OF_SECOND, nano, position, successPos);
3304 }
3305
3306 @Override
3307 public String toString() {
3308 return "Instant()";
3309 }
3310 }
3311
3312 //-----------------------------------------------------------------------
3313 /**
3314 * Prints or parses an offset ID.
3315 */
3316 static final class OffsetIdPrinterParser implements DateTimePrinterParser {
3317 static final String[] PATTERNS = new String[] {
3318 "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS",
3319 }; // order used in pattern builder
3320 static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z");
3321 static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0");
3322
3323 private final String noOffsetText;
3324 private final int type;
3325
3326 /**
3327 * Constructor.
3328 *
3329 * @param pattern the pattern
3330 * @param noOffsetText the text to use for UTC, not null
3331 */
3332 OffsetIdPrinterParser(String pattern, String noOffsetText) {
3333 Objects.requireNonNull(pattern, "pattern");
3334 Objects.requireNonNull(noOffsetText, "noOffsetText");
3335 this.type = checkPattern(pattern);
3336 this.noOffsetText = noOffsetText;
3337 }
3338
3345 throw new IllegalArgumentException("Invalid zone offset pattern: " + pattern);
3346 }
3347
3348 @Override
3349 public boolean format(DateTimePrintContext context, StringBuilder buf) {
3350 Long offsetSecs = context.getValue(OFFSET_SECONDS);
3351 if (offsetSecs == null) {
3352 return false;
3353 }
3354 int totalSecs = Math.toIntExact(offsetSecs);
3355 if (totalSecs == 0) {
3356 buf.append(noOffsetText);
3357 } else {
3358 int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped
3359 int absMinutes = Math.abs((totalSecs / 60) % 60);
3360 int absSeconds = Math.abs(totalSecs % 60);
3361 int bufPos = buf.length();
3362 int output = absHours;
3363 buf.append(totalSecs < 0 ? "-" : "+")
3364 .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0'));
3365 if (type >= 3 || (type >= 1 && absMinutes > 0)) {
3366 buf.append((type % 2) == 0 ? ":" : "")
3367 .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0'));
3368 output += absMinutes;
3369 if (type >= 7 || (type >= 5 && absSeconds > 0)) {
3370 buf.append((type % 2) == 0 ? ":" : "")
3371 .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0'));
3372 output += absSeconds;
3373 }
3374 }
3375 if (output == 0) {
3376 buf.setLength(bufPos);
3377 buf.append(noOffsetText);
3378 }
3379 }
3380 return true;
3381 }
3382
3383 @Override
3384 public int parse(DateTimeParseContext context, CharSequence text, int position) {
3385 int length = text.length();
3386 int noOffsetLen = noOffsetText.length();
3387 if (noOffsetLen == 0) {
3388 if (position == length) {
3389 return context.setParsedField(OFFSET_SECONDS, 0, position, position);
3390 }
3391 } else {
3392 if (position == length) {
3393 return ~position;
3394 }
3395 if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) {
3396 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen);
3397 }
3398 }
3399
3400 // parse normal plus/minus offset
3401 char sign = text.charAt(position); // IOOBE if invalid position
3402 if (sign == '+' || sign == '-') {
3403 // starts
3404 int negative = (sign == '-' ? -1 : 1);
3405 int[] array = new int[4];
3406 array[0] = position + 1;
3407 if ((parseNumber(array, 1, text, true) ||
3408 parseNumber(array, 2, text, type >=3) ||
3409 parseNumber(array, 3, text, false)) == false) {
3410 // success
3411 long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]);
3412 return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]);
3413 }
3414 }
3415 // handle special case of empty no offset text
3416 if (noOffsetLen == 0) {
3417 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen);
3418 }
3419 return ~position;
3420 }
3421
3422 /**
3423 * Parse a two digit zero-prefixed number.
3424 *
3425 * @param array the array of parsed data, 0=pos,1=hours,2=mins,3=secs, not null
3426 * @param arrayIndex the index to parse the value into
3427 * @param parseText the offset ID, not null
3428 * @param required whether this number is required
3429 * @return true if an error occurred
3430 */
3431 private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required) {
3432 if ((type + 3) / 2 < arrayIndex) {
3433 return false; // ignore seconds/minutes
3434 }
3435 int pos = array[0];
3436 if ((type % 2) == 0 && arrayIndex > 1) {
3437 if (pos + 1 > parseText.length() || parseText.charAt(pos) != ':') {
3438 return required;
3439 }
3440 pos++;
3441 }
3442 if (pos + 2 > parseText.length()) {
3443 return required;
3444 }
3445 char ch1 = parseText.charAt(pos++);
3446 char ch2 = parseText.charAt(pos++);
3447 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
3448 return required;
3449 }
3450 int value = (ch1 - 48) * 10 + (ch2 - 48);
3451 if (value < 0 || value > 59) {
3452 return required;
3453 }
3454 array[arrayIndex] = value;
3455 array[0] = pos;
3456 return false;
|
849 * epoch-seconds value. That is achieved using {@code appendValue(INSTANT_SECONDS)}.
850 *
851 * @param fractionalDigits the number of fractional second digits to format with,
852 * from 0 to 9, or -1 to use as many digits as necessary
853 * @return this, for chaining, not null
854 * @throws IllegalArgumentException if the number of fractional digits is invalid
855 */
856 public DateTimeFormatterBuilder appendInstant(int fractionalDigits) {
857 if (fractionalDigits < -1 || fractionalDigits > 9) {
858 throw new IllegalArgumentException("The fractional digits must be from -1 to 9 inclusive but was " + fractionalDigits);
859 }
860 appendInternal(new InstantPrinterParser(fractionalDigits));
861 return this;
862 }
863
864 //-----------------------------------------------------------------------
865 /**
866 * Appends the zone offset, such as '+01:00', to the formatter.
867 * <p>
868 * This appends an instruction to format/parse the offset ID to the builder.
869 * This is equivalent to calling {@code appendOffset("+HH:mm:ss", "Z")}.
870 * See {@link #appendOffset(String, String)} for details on formatting
871 * and parsing.
872 *
873 * @return this, for chaining, not null
874 */
875 public DateTimeFormatterBuilder appendOffsetId() {
876 appendInternal(OffsetIdPrinterParser.INSTANCE_ID_Z);
877 return this;
878 }
879
880 /**
881 * Appends the zone offset, such as '+01:00', to the formatter.
882 * <p>
883 * This appends an instruction to format/parse the offset ID to the builder.
884 * <p>
885 * During formatting, the offset is obtained using a mechanism equivalent
886 * to querying the temporal with {@link TemporalQueries#offset()}.
887 * It will be printed using the format defined below.
888 * If the offset cannot be obtained then an exception is thrown unless the
889 * section of the formatter is optional.
890 * <p>
891 * When parsing in strict mode, the input must contain the mandatory
892 * and optional elements are defined by the specified pattern.
893 * If the offset cannot be parsed then an exception is thrown unless
894 * the section of the formatter is optional.
895 * <p>
896 * When parsing in lenient mode, only the hours are mandatory - minutes
897 * and seconds are optional.
898 * The colons are required if the specified pattern contains a colon.
899 * If the specified pattern is "+HH", the presence of colons is
900 * determined by whether the character after the hour digits is a colon
901 * or not.
902 * If the offset cannot be parsed then an exception is thrown unless
903 * the section of the formatter is optional.
904 * <p>
905 * The format of the offset is controlled by a pattern which must be one
906 * of the following:
907 * <ul>
908 * <li>{@code +HH} - hour only, ignoring minute and second
909 * <li>{@code +HHmm} - hour, with minute if non-zero, ignoring second, no colon
910 * <li>{@code +HH:mm} - hour, with minute if non-zero, ignoring second, with colon
911 * <li>{@code +HHMM} - hour and minute, ignoring second, no colon
912 * <li>{@code +HH:MM} - hour and minute, ignoring second, with colon
913 * <li>{@code +HHMMss} - hour and minute, with second if non-zero, no colon
914 * <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon
915 * <li>{@code +HHMMSS} - hour, minute and second, no colon
916 * <li>{@code +HH:MM:SS} - hour, minute and second, with colon
917 * <li>{@code +HHmmss} - hour, with minute if non-zero or with minute and
918 * second if non-zero, no colon
919 * <li>{@code +HH:mm:ss} - hour, with minute if non-zero or with minute and
920 * second if non-zero, with colon
921 * </ul>
922 * The "no offset" text controls what text is printed when the total amount of
923 * the offset fields to be output is zero.
924 * Example values would be 'Z', '+00:00', 'UTC' or 'GMT'.
925 * Three formats are accepted for parsing UTC - the "no offset" text, and the
926 * plus and minus versions of zero defined by the pattern.
927 *
928 * @param pattern the pattern to use, not null
929 * @param noOffsetText the text to use when the offset is zero, not null
930 * @return this, for chaining, not null
931 * @throws IllegalArgumentException if the pattern is invalid
932 */
933 public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) {
934 appendInternal(new OffsetIdPrinterParser(pattern, noOffsetText));
935 return this;
936 }
937
938 /**
939 * Appends the localized zone offset, such as 'GMT+01:00', to the formatter.
940 * <p>
3314 } catch (RuntimeException ex) {
3315 return ~position;
3316 }
3317 int successPos = pos;
3318 successPos = context.setParsedField(INSTANT_SECONDS, instantSecs, position, successPos);
3319 return context.setParsedField(NANO_OF_SECOND, nano, position, successPos);
3320 }
3321
3322 @Override
3323 public String toString() {
3324 return "Instant()";
3325 }
3326 }
3327
3328 //-----------------------------------------------------------------------
3329 /**
3330 * Prints or parses an offset ID.
3331 */
3332 static final class OffsetIdPrinterParser implements DateTimePrinterParser {
3333 static final String[] PATTERNS = new String[] {
3334 "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", "+HHmmss", "+HH:mm:ss",
3335 }; // order used in pattern builder
3336 static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z");
3337 static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0");
3338
3339 private final String noOffsetText;
3340 private final int type;
3341
3342 /**
3343 * Constructor.
3344 *
3345 * @param pattern the pattern
3346 * @param noOffsetText the text to use for UTC, not null
3347 */
3348 OffsetIdPrinterParser(String pattern, String noOffsetText) {
3349 Objects.requireNonNull(pattern, "pattern");
3350 Objects.requireNonNull(noOffsetText, "noOffsetText");
3351 this.type = checkPattern(pattern);
3352 this.noOffsetText = noOffsetText;
3353 }
3354
3361 throw new IllegalArgumentException("Invalid zone offset pattern: " + pattern);
3362 }
3363
3364 @Override
3365 public boolean format(DateTimePrintContext context, StringBuilder buf) {
3366 Long offsetSecs = context.getValue(OFFSET_SECONDS);
3367 if (offsetSecs == null) {
3368 return false;
3369 }
3370 int totalSecs = Math.toIntExact(offsetSecs);
3371 if (totalSecs == 0) {
3372 buf.append(noOffsetText);
3373 } else {
3374 int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped
3375 int absMinutes = Math.abs((totalSecs / 60) % 60);
3376 int absSeconds = Math.abs(totalSecs % 60);
3377 int bufPos = buf.length();
3378 int output = absHours;
3379 buf.append(totalSecs < 0 ? "-" : "+")
3380 .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0'));
3381 if ((type >= 3 && type < 9) || (type >= 9 && absSeconds > 0) || (type >= 1 && absMinutes > 0)) {
3382 buf.append((type % 2) == 0 ? ":" : "")
3383 .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0'));
3384 output += absMinutes;
3385 if (type == 7 || type == 8 || (type >= 5 && absSeconds > 0)) {
3386 buf.append((type % 2) == 0 ? ":" : "")
3387 .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0'));
3388 output += absSeconds;
3389 }
3390 }
3391 if (output == 0) {
3392 buf.setLength(bufPos);
3393 buf.append(noOffsetText);
3394 }
3395 }
3396 return true;
3397 }
3398
3399 @Override
3400 public int parse(DateTimeParseContext context, CharSequence text, int position) {
3401 int length = text.length();
3402 int noOffsetLen = noOffsetText.length();
3403 int parseType = type;
3404 if (context.isStrict() == false) {
3405 parseType = 9;
3406 if ((length > position + 3) && (text.charAt(position + 3) == ':')) {
3407 parseType = 10;
3408 }
3409 }
3410 if (noOffsetLen == 0) {
3411 if (position == length) {
3412 return context.setParsedField(OFFSET_SECONDS, 0, position, position);
3413 }
3414 } else {
3415 if (position == length) {
3416 return ~position;
3417 }
3418 if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) {
3419 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen);
3420 }
3421 }
3422
3423 // parse normal plus/minus offset
3424 char sign = text.charAt(position); // IOOBE if invalid position
3425 if (sign == '+' || sign == '-') {
3426 // starts
3427 int negative = (sign == '-' ? -1 : 1);
3428 int[] array = new int[4];
3429 array[0] = position + 1;
3430 if ((parseNumber(array, 1, text, true, parseType) ||
3431 parseNumber(array, 2, text, parseType >= 3 && parseType < 9, parseType) ||
3432 parseNumber(array, 3, text, parseType == 7 || parseType == 8, parseType)) == false) {
3433 // success
3434 long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]);
3435 return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]);
3436 }
3437 }
3438 // handle special case of empty no offset text
3439 if (noOffsetLen == 0) {
3440 return context.setParsedField(OFFSET_SECONDS, 0, position, position);
3441 }
3442 return ~position;
3443 }
3444
3445 /**
3446 * Parse a two digit zero-prefixed number.
3447 *
3448 * @param array the array of parsed data, 0=pos,1=hours,2=mins,3=secs, not null
3449 * @param arrayIndex the index to parse the value into
3450 * @param parseText the offset ID, not null
3451 * @param required whether this number is required
3452 * @param parseType the offset pattern type
3453 * @return true if an error occurred
3454 */
3455 private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required, int parseType) {
3456 if ((parseType + 3) / 2 < arrayIndex) {
3457 return false; // ignore seconds/minutes
3458 }
3459 int pos = array[0];
3460 if ((parseType % 2) == 0 && arrayIndex > 1) {
3461 if (pos + 1 > parseText.length() || parseText.charAt(pos) != ':') {
3462 return required;
3463 }
3464 pos++;
3465 }
3466 if (pos + 2 > parseText.length()) {
3467 return required;
3468 }
3469 char ch1 = parseText.charAt(pos++);
3470 char ch2 = parseText.charAt(pos++);
3471 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
3472 return required;
3473 }
3474 int value = (ch1 - 48) * 10 + (ch2 - 48);
3475 if (value < 0 || value > 59) {
3476 return required;
3477 }
3478 array[arrayIndex] = value;
3479 array[0] = pos;
3480 return false;
|