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. The colons are required if the specified
898 * pattern contains a colon. If the specified pattern is "+HH", the
899 * presence of colons is determined by whether the character after the
900 * hour digits is a colon or not.
901 * If the offset cannot be parsed then an exception is thrown unless
902 * the section of the formatter is optional.
903 * <p>
904 * The format of the offset is controlled by a pattern which must be one
905 * of the following:
906 * <ul>
907 * <li>{@code +HH} - hour only, ignoring minute and second
908 * <li>{@code +HHmm} - hour, with minute if non-zero, ignoring second, no colon
909 * <li>{@code +HH:mm} - hour, with minute if non-zero, ignoring second, with colon
910 * <li>{@code +HHMM} - hour and minute, ignoring second, no colon
911 * <li>{@code +HH:MM} - hour and minute, ignoring second, with colon
912 * <li>{@code +HHMMss} - hour and minute, with second if non-zero, no colon
913 * <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon
914 * <li>{@code +HHMMSS} - hour, minute and second, no colon
915 * <li>{@code +HH:MM:SS} - hour, minute and second, with colon
916 * <li>{@code +HHmmss} - hour, with minute if non-zero or with minute and
917 * second if non-zero, no colon
918 * <li>{@code +HH:mm:ss} - hour, with minute if non-zero or with minute and
919 * second if non-zero, with colon
920 * </ul>
921 * The "no offset" text controls what text is printed when the total amount of
922 * the offset fields to be output is zero.
923 * Example values would be 'Z', '+00:00', 'UTC' or 'GMT'.
924 * Three formats are accepted for parsing UTC - the "no offset" text, and the
925 * plus and minus versions of zero defined by the pattern.
926 *
927 * @param pattern the pattern to use, not null
928 * @param noOffsetText the text to use when the offset is zero, not null
929 * @return this, for chaining, not null
930 * @throws IllegalArgumentException if the pattern is invalid
931 */
932 public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText) {
933 appendInternal(new OffsetIdPrinterParser(pattern, noOffsetText));
934 return this;
935 }
936
937 /**
938 * Appends the localized zone offset, such as 'GMT+01:00', to the formatter.
939 * <p>
3313 } catch (RuntimeException ex) {
3314 return ~position;
3315 }
3316 int successPos = pos;
3317 successPos = context.setParsedField(INSTANT_SECONDS, instantSecs, position, successPos);
3318 return context.setParsedField(NANO_OF_SECOND, nano, position, successPos);
3319 }
3320
3321 @Override
3322 public String toString() {
3323 return "Instant()";
3324 }
3325 }
3326
3327 //-----------------------------------------------------------------------
3328 /**
3329 * Prints or parses an offset ID.
3330 */
3331 static final class OffsetIdPrinterParser implements DateTimePrinterParser {
3332 static final String[] PATTERNS = new String[] {
3333 "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", "+HHmmss", "+HH:mm:ss",
3334 }; // order used in pattern builder
3335 static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z");
3336 static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0");
3337
3338 private final String noOffsetText;
3339 private final int type;
3340
3341 /**
3342 * Constructor.
3343 *
3344 * @param pattern the pattern
3345 * @param noOffsetText the text to use for UTC, not null
3346 */
3347 OffsetIdPrinterParser(String pattern, String noOffsetText) {
3348 Objects.requireNonNull(pattern, "pattern");
3349 Objects.requireNonNull(noOffsetText, "noOffsetText");
3350 this.type = checkPattern(pattern);
3351 this.noOffsetText = noOffsetText;
3352 }
3353
3360 throw new IllegalArgumentException("Invalid zone offset pattern: " + pattern);
3361 }
3362
3363 @Override
3364 public boolean format(DateTimePrintContext context, StringBuilder buf) {
3365 Long offsetSecs = context.getValue(OFFSET_SECONDS);
3366 if (offsetSecs == null) {
3367 return false;
3368 }
3369 int totalSecs = Math.toIntExact(offsetSecs);
3370 if (totalSecs == 0) {
3371 buf.append(noOffsetText);
3372 } else {
3373 int absHours = Math.abs((totalSecs / 3600) % 100); // anything larger than 99 silently dropped
3374 int absMinutes = Math.abs((totalSecs / 60) % 60);
3375 int absSeconds = Math.abs(totalSecs % 60);
3376 int bufPos = buf.length();
3377 int output = absHours;
3378 buf.append(totalSecs < 0 ? "-" : "+")
3379 .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0'));
3380 if ((type >= 3 && type < 9) || (type >= 9 && absSeconds > 0) || (type >= 1 && absMinutes > 0)) {
3381 buf.append((type % 2) == 0 ? ":" : "")
3382 .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0'));
3383 output += absMinutes;
3384 if (type == 7 || type == 8 || (type >= 5 && absSeconds > 0)) {
3385 buf.append((type % 2) == 0 ? ":" : "")
3386 .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0'));
3387 output += absSeconds;
3388 }
3389 }
3390 if (output == 0) {
3391 buf.setLength(bufPos);
3392 buf.append(noOffsetText);
3393 }
3394 }
3395 return true;
3396 }
3397
3398 @Override
3399 public int parse(DateTimeParseContext context, CharSequence text, int position) {
3400 int length = text.length();
3401 int noOffsetLen = noOffsetText.length();
3402 int parseType = type;
3403 if (context.isStrict() == false) {
3404 if ((parseType > 0 && (parseType % 2) == 0) ||
3405 (parseType == 0 && length > position + 3 && text.charAt(position + 3) == ':')) {
3406 parseType = 10;
3407 } else {
3408 parseType = 9;
3409 }
3410 }
3411 if (noOffsetLen == 0) {
3412 if (position == length) {
3413 return context.setParsedField(OFFSET_SECONDS, 0, position, position);
3414 }
3415 } else {
3416 if (position == length) {
3417 return ~position;
3418 }
3419 if (context.subSequenceEquals(text, position, noOffsetText, 0, noOffsetLen)) {
3420 return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen);
3421 }
3422 }
3423
3424 // parse normal plus/minus offset
3425 char sign = text.charAt(position); // IOOBE if invalid position
3426 if (sign == '+' || sign == '-') {
3427 // starts
3428 int negative = (sign == '-' ? -1 : 1);
3429 int[] array = new int[4];
3430 array[0] = position + 1;
3431 if ((parseNumber(array, 1, text, true, parseType) ||
3432 parseNumber(array, 2, text, parseType >= 3 && parseType < 9, parseType) ||
3433 parseNumber(array, 3, text, parseType == 7 || parseType == 8, parseType)) == false) {
3434 // success
3435 long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]);
3436 return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]);
3437 }
3438 }
3439 // handle special case of empty no offset text
3440 if (noOffsetLen == 0) {
3441 return context.setParsedField(OFFSET_SECONDS, 0, position, position);
3442 }
3443 return ~position;
3444 }
3445
3446 /**
3447 * Parse a two digit zero-prefixed number.
3448 *
3449 * @param array the array of parsed data, 0=pos,1=hours,2=mins,3=secs, not null
3450 * @param arrayIndex the index to parse the value into
3451 * @param parseText the offset ID, not null
3452 * @param required whether this number is required
3453 * @param parseType the offset pattern type
3454 * @return true if an error occurred
3455 */
3456 private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required, int parseType) {
3457 if ((parseType + 3) / 2 < arrayIndex) {
3458 return false; // ignore seconds/minutes
3459 }
3460 int pos = array[0];
3461 if ((parseType % 2) == 0 && arrayIndex > 1) {
3462 if (pos + 1 > parseText.length() || parseText.charAt(pos) != ':') {
3463 return required;
3464 }
3465 pos++;
3466 }
3467 if (pos + 2 > parseText.length()) {
3468 return required;
3469 }
3470 char ch1 = parseText.charAt(pos++);
3471 char ch2 = parseText.charAt(pos++);
3472 if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
3473 return required;
3474 }
3475 int value = (ch1 - 48) * 10 + (ch2 - 48);
3476 if (value < 0 || value > 59) {
3477 return required;
3478 }
3479 array[arrayIndex] = value;
3480 array[0] = pos;
3481 return false;
|