Home Reference Source

packages/core/src/format/DateTimeFormatter.js

/**
 * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
 * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 * @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
 */

import { assert, requireNonNull } from '../assert';

import { DateTimeParseException, NullPointerException } from '../errors';

import { Period } from '../Period';

import { ParsePosition } from './ParsePosition';
import { DateTimeBuilder } from './DateTimeBuilder';
import { DateTimeParseContext } from './DateTimeParseContext';
import { DateTimePrintContext } from './DateTimePrintContext';
import { DateTimeFormatterBuilder } from './DateTimeFormatterBuilder';
import { SignStyle } from './SignStyle';
import { StringBuilder } from './StringBuilder';
import { ResolverStyle } from './ResolverStyle';

import { IsoChronology } from '../chrono/IsoChronology';
import { ChronoField } from '../temporal/ChronoField';
import { createTemporalQuery } from '../temporal/TemporalQuery';

/**
 *
 * ### Static properties of Class {@link DateTimeFormatter}
 *
 * DateTimeFormatter.ISO_LOCAL_DATE
 *
 * DateTimeFormatter.ISO_LOCAL_TIME
 *
 * DateTimeFormatter.ISO_LOCAL_DATE_TIME
 *
 */
export class DateTimeFormatter {

    //-----------------------------------------------------------------------
    /**
     * A query that provides access to the excess days that were parsed.
     *
     * This returns a singleton {@link TemporalQuery} that provides
     * access to additional information from the parse. The query always returns
     * a non-null period, with a zero period returned instead of null.
     *
     * There are two situations where this query may return a non-zero period.
     *
     * * If the {@link ResolverStyle} is {@link LENIENT} and a time is parsed
     *   without a date, then the complete result of the parse consists of a
     *   {@link LocalTime} and an excess {@link Period} in days.
     * * If the {@link ResolverStyle} is {@link SMART} and a time is parsed
     *   without a date where the time is 24:00:00, then the complete result of
     *   the parse consists of a {@link LocalTime} of 00:00:00 and an excess
     *   {@link Period} of one day.
     *
     * In both cases, if a complete {@link ChronoLocalDateTime} or {@link Instant}
     * is parsed, then the excess days are added to the date part.
     * As a result, this query will return a zero period.
     *
     * The {@link SMART} behaviour handles the common "end of day" 24:00 value.
     * Processing in {@link LENIENT} mode also produces the same result:
     * <pre>
     *  Text to parse        Parsed object                         Excess days
     *  "2012-12-03T00:00"   LocalDateTime.of(2012, 12, 3, 0, 0)   ZERO
     *  "2012-12-03T24:00"   LocalDateTime.of(2012, 12, 4, 0, 0)   ZERO
     *  "00:00"              LocalTime.of(0, 0)                    ZERO
     *  "24:00"              LocalTime.of(0, 0)                    Period.ofDays(1)
     * </pre>
     * The query can be used as follows:
     * <pre>
     *  TemporalAccessor parsed = formatter.parse(str);
     *  LocalTime time = parsed.query(LocalTime.FROM);
     *  Period extraDays = parsed.query(DateTimeFormatter.parsedExcessDays());
     * </pre>
     * @return {TemporalQuery} a query that provides access to the excess days that were parsed
     */
    static parsedExcessDays() {
        return DateTimeFormatter.PARSED_EXCESS_DAYS;
    }

    /**
     * A query that provides access to whether a leap-second was parsed.
     *
     * This returns a singleton {@link TemporalQuery} that provides
     * access to additional information from the parse. The query always returns
     * a non-null boolean, true if parsing saw a leap-second, false if not.
     *
     * Instant parsing handles the special "leap second" time of '23:59:60'.
     * Leap seconds occur at '23:59:60' in the UTC time-zone, but at other
     * local times in different time-zones. To avoid this potential ambiguity,
     * the handling of leap-seconds is limited to
     * {@link DateTimeFormatterBuilder#appendInstant}, as that method
     * always parses the instant with the UTC zone offset.
     *
     * If the time '23:59:60' is received, then a simple conversion is applied,
     * replacing the second-of-minute of 60 with 59. This query can be used
     * on the parse result to determine if the leap-second adjustment was made.
     * The query will return one second of excess if it did adjust to remove
     * the leap-second, and zero if not. Note that applying a leap-second
     * smoothing mechanism, such as UTC-SLS, is the responsibility of the
     * application, as follows:
     * <pre>
     *  TemporalAccessor parsed = formatter.parse(str);
     *  Instant instant = parsed.query(Instant::from);
     *  if (parsed.query(DateTimeFormatter.parsedLeapSecond())) {
     *    // validate leap-second is correct and apply correct smoothing
     *  }
     * </pre>
     * @return a query that provides access to whether a leap-second was parsed
     */
    static parsedLeapSecond() {
        return DateTimeFormatter.PARSED_LEAP_SECOND;
    }

    /**
     * Creates a formatter using the specified pattern.
     *
     * This method will create a formatter based on a simple pattern of letters and symbols.
     *
     * The returned formatter will use the default locale, but this can be changed
     * using {@link DateTimeFormatter.withLocale}.
     *
     * 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
     *  | u      | year                       | year             | 2004; 04
     *  | y      | year-of-era                | 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-year               | number           | 27
     *  | W      | week-of-month              | number           | 27
     *  | e      | localized day-of-week      | number           | 2; Tue; Tuesday; T
     *  | 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
     *  |        |                            |                  |
     *  | V      | time-zone ID               | zone-id          | America/Los_Angeles; Z; -08:30
     *  | z      | time-zone name             | zone-name        | Pacific Standard Time; PST
     *  | X      | zone-offset 'Z' for zero   | offset-X         | Z; -08; -0830; -08:30; -083015; -08:30:15;
     *  | x      | zone-offset                | offset-x         | +0000; -08; -0830; -08:30; -083015; -08:30:15;
     *  | Z      | zone-offset                | offset-Z         | +0000; -0800; -08:00;
     *  |        |                            |                  |
     *  | p      | pad next                   | pad modifier     | 1
     *  |        |                            |                  |
     *  | '      | escape for text            | delimiter        |
     *  | ''     | single quote               | literal          | '
     *  | [      | optional section start     |                  |
     *  | ]      | optional section end       |                  |
     *  | {}     | reserved for future use    |                  |
     * </pre>
     *
     * The count of pattern letters determine the format.
     *
     * **Text**: The text style is determined based on the number of pattern letters used.
     * Less than 4 pattern letters will use the short form `TextStyle.SHORT`.
     * Exactly 4 pattern letters will use the full form `TextStyle.FULL`.
     * Exactly 5 pattern letters will use the narrow form `TextStyle.NARROW`.
     *
     * **NOTE**: since text styles require locale support, they are currently not supported in js-joda!
     *
     * **Number**: If the count of letters is one, then the value is printed using the minimum number
     * of digits and without padding as per {@link DateTimeFormatterBuilder.appendValue}.
     * Otherwise, the count of digits is used as the width of the output field as per
     * {@link DateTimeFormatterBuilder.appendValue}.
     *
     * **Number/Text**: If the count of pattern letters is 3 or greater, use the Text rules above.
     * Otherwise use the Number rules above.
     *
     * **Fraction**: 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.
     *
     * **Year**: The count of letters determines the minimum field width below which padding is used.
     * If the count of letters is two, then a {@link DateTimeFormatterBuilder.appendValueReduced}
     * 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 `SignStyle.NORMAL`.
     * Otherwise, the sign is output if the pad width is exceeded, as per `SignStyle.EXCEEDS_PAD`
     *
     * **ZoneId**: This outputs the time-zone ID, such as 'Europe/Paris'.
     * If the count of letters is two, then the time-zone ID is output.
     * Any other count of letters throws `IllegalArgumentException`.
     *
     * **Zone names**: This outputs the display name of the time-zone ID.
     * If the count of letters is one, two or three, then the short name is output.
     * If the count of letters is four, then the full name is output.
     * Five or more letters throws `IllegalArgumentException`.
     *
     * **NOTE**: since zone ids and name require the iana tzdb, they are currently not supported in js-joda!
     *
     * **Offset X and x**: This formats the offset based on the number of pattern letters.
     * One letter outputs just the hour', such as '+01', unless the minute is non-zero
     * in which case the minute is also output, such as '+0130'.
     * 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'.
     * Six or more letters throws `IllegalArgumentException`.
     * Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero,
     * whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'.
     *
     * **Offset Z**: This formats the offset based on the number of pattern letters.
     * One, two or three letters outputs the hour and minute, without a colon, such as '+0130'.
     * Four or more letters throws `IllegalArgumentException`.
     * The output will be '+0000' when the offset is zero.
     *
     * **Optional section**: The optional section markers work exactly like calling
     * {@link DateTimeFormatterBuilder.optionalStart} and {@link DateTimeFormatterBuilder.optionalEnd}.
     *
     * **Pad modifier**: 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 DateTimeFormatterBuilder.padNext}.
     *
     * For example, 'ppH' outputs the hour-of-day padded on the left with spaces to a width of 2.
     *
     * 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.
     *
     * @param {String} pattern  the pattern to use, not null
     * @return {DateTimeFormatter} the formatter based on the pattern, not null
     * @throws IllegalArgumentException if the pattern is invalid
     * @see DateTimeFormatterBuilder#appendPattern(String)
     * @example
     * var s = LocalDate.parse('2016-04-01').format(DateTimeFormatter.ofPattern('d MM yyyy'));
     * console.log(s); // '1 04 2016'
     *
     */
    static ofPattern(pattern) {
        return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
    }


    //-----------------------------------------------------------------------
    /**
     * Constructor.
     *
     * @param printerParser  the printer/parser to use, not null
     * @param locale  the locale to use, not null
     * @param decimalStyle  the decimal style to use, not null
     * @param resolverStyle  the resolver style to use, not null
     * @param resolverFields  the fields to use during resolving, null for all fields
     * @param chrono  the chronology to use, null for no override
     * @param zone  the zone to use, null for no override
     * @private
     */
    constructor(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono=IsoChronology.INSTANCE, zone) {
        assert(printerParser != null);
        assert(decimalStyle != null);
        assert(resolverStyle != null);
        /**
         * The printer and/or parser to use, not null.
         */
        this._printerParser = printerParser;
        /**
         * The locale to use for formatting. // nyi
         */
        this._locale = locale;
        /**
         * The symbols to use for formatting, not null.
         */
        this._decimalStyle = decimalStyle;
        /**
         * The resolver style to use, not null.
         */
        this._resolverStyle = resolverStyle;
        /**
         * The fields to use in resolving, null for all fields.
         */
        this._resolverFields = resolverFields;
        /**
         * The chronology to use for formatting, null for no override.
         */
        this._chrono = chrono;
        /**
         * The zone to use for formatting, null for no override. // nyi
         */
        this._zone = zone;
    }

    locale() {
        return this._locale;
    }

    decimalStyle() {
        return this._decimalStyle;
    }

    chronology() {
        return this._chrono;
    }

    /**
     * Returns a copy of this formatter with a new override chronology.
     *
     * This returns a formatter with similar state to this formatter but
     * with the override chronology set.
     * By default, a formatter has no override chronology, returning null.
     *
     * If an override is added, then any date that is printed or parsed will be affected.
     *
     * When printing, if the {@link Temporal} object contains a date then it will
     * be converted to a date in the override chronology.
     * Any time or zone will be retained unless overridden.
     * The converted result will behave in a manner equivalent to an implementation
     * of {@link ChronoLocalDate},{@link ChronoLocalDateTime} or {@link ChronoZonedDateTime}.
     *
     * When parsing, the override chronology will be used to interpret the
     * {@link ChronoField} into a date unless the
     * formatter directly parses a valid chronology.
     *
     * This instance is immutable and unaffected by this method call.
     *
     * @param chrono  the new chronology, not null
     * @return a formatter based on this formatter with the requested override chronology, not null
     */
    withChronology(chrono) {
        if (this._chrono != null && this._chrono.equals(chrono)) {
            return this;
        }
        return new DateTimeFormatter(this._printerParser, this._locale, this._decimalStyle,
            this._resolverStyle, this._resolverFields, chrono, this._zone);
    }

    /**
     * not yet supported
     * @returns {DateTimeFormatter}
     */
    withLocale(){
        return this;
    }

    /**
     * Returns a copy of this formatter with a new resolver style.
     * <p>
     * This returns a formatter with similar state to this formatter but
     * with the resolver style set. By default, a formatter has the
     * {@link ResolverStyle#SMART SMART} resolver style.
     * <p>
     * Changing the resolver style only has an effect during parsing.
     * Parsing a text string occurs in two phases.
     * Phase 1 is a basic text parse according to the fields added to the builder.
     * Phase 2 resolves the parsed field-value pairs into date and/or time objects.
     * The resolver style is used to control how phase 2, resolving, happens.
     * See {@link ResolverStyle} for more information on the options available.
     * <p>
     * This instance is immutable and unaffected by this method call.
     *
     * @param {ResolverStyle} resolverStyle  the new resolver style, not null
     * @return {DateTimeFormatter} a formatter based on this formatter with the requested resolver style, not null
     */
    withResolverStyle(resolverStyle) {
        requireNonNull(resolverStyle, 'resolverStyle');
        if (resolverStyle.equals(this._resolverStyle)) {
            return this;
        }
        return new DateTimeFormatter(this._printerParser, this._locale, this._decimalStyle, resolverStyle, this._resolverFields, this._chrono, this._zone);
    }
    //-----------------------------------------------------------------------
    /**
     * Formats a date-time object using this formatter.
     *
     * This formats the date-time to a String using the rules of the formatter.
     *
     * @param {TemporalAccessor} temporal  the temporal object to print, not null
     * @return {String} the printed string, not null
     * @throws DateTimeException if an error occurs during formatting
     */
    format(temporal) {
        const buf = new StringBuilder(32);
        this._formatTo(temporal, buf);
        return buf.toString();
    }

    //-----------------------------------------------------------------------
    /**
     * Formats a date-time object to an {@link Appendable} using this formatter.
     *
     * This formats the date-time to the specified destination.
     * {@link Appendable} is a general purpose interface that is implemented by all
     * key character output classes including {@link StringBuffer}, {@link StringBuilder},
     * {@link PrintStream} and {@link Writer}.
     *
     * Although {@link Appendable} methods throw an {@link IOException}, this method does not.
     * Instead, any {@link IOException} is wrapped in a runtime exception.
     *
     * @param {TemporalAccessor} temporal - the temporal object to print, not null
     * @param {StringBuilder} appendable - the appendable to print to, not null
     * @throws DateTimeException if an error occurs during formatting
     */
    _formatTo(temporal, appendable) {
        requireNonNull(temporal, 'temporal');
        requireNonNull(appendable, 'appendable');
        const context = new DateTimePrintContext(temporal, this);
        this._printerParser.print(context, appendable);
    }

    /**
     * function overloading for {@link DateTimeFormatter.parse}
     *
     * if called with one arg {@link DateTimeFormatter.parse1} is called
     * otherwise {@link DateTimeFormatter.parse2}
     *
     * @param {string} text
     * @param {TemporalQuery} type
     * @return {TemporalAccessor}
     */
    parse(text, type){
        if(arguments.length === 1){
            return this.parse1(text);
        } else {
            return this.parse2(text, type);
        }
    }

    /**
     * Fully parses the text producing a temporal object.
     *
     * This parses the entire text producing a temporal object.
     * It is typically more useful to use {@link parse}.
     * The result of this method is {@link TemporalAccessor} which has been resolved,
     * applying basic validation checks to help ensure a valid date-time.
     *
     * If the parse completes without reading the entire length of the text,
     * or a problem occurs during parsing or merging, then an exception is thrown.
     *
     * @param {String} text  the text to parse, not null
     * @return {TemporalAccessor} the parsed temporal object, not null
     * @throws DateTimeParseException if unable to parse the requested result
     */
    parse1(text) {
        requireNonNull(text, 'text');
        try {
            return this._parseToBuilder(text, null).resolve(this._resolverStyle, this._resolverFields);
        } catch (ex) {
            if(ex instanceof DateTimeParseException){
                throw ex;
            } else {
                throw this._createError(text, ex);
            }
        }
    }

    /**
     * Fully parses the text producing a temporal object.
     *
     * This parses the entire text producing a temporal object.
     * It is typically more useful to use {@link parse}.
     * The result of this method is {@link TemporalAccessor} which has been resolved,
     * applying basic validation checks to help ensure a valid date-time.
     *
     * If the parse completes without reading the entire length of the text,
     * or a problem occurs during parsing or merging, then an exception is thrown.
     *
     * @param text  the text to parse, not null
     * @param type the type to extract, not null
 * @return the parsed temporal object, not null
     * @throws DateTimeParseException if unable to parse the requested result
     */
    parse2(text, type) {
        requireNonNull(text, 'text');
        requireNonNull(type, 'type');
        try {
            const builder = this._parseToBuilder(text, null).resolve(this._resolverStyle, this._resolverFields);
            return builder.build(type);
        } catch (ex) {
            if(ex instanceof DateTimeParseException){
                throw ex;
            } else {
                throw this._createError(text, ex);
            }
        }
    }

    _createError(text, ex) {
        let abbr = '';
        if (text.length > 64) {
            abbr = `${text.substring(0, 64)}...`;
        } else {
            abbr = text;
        }
        return new DateTimeParseException(`Text '${abbr}' could not be parsed: ${ex.message}`, text, 0, ex);
    }


    /**
     * Parses the text to a builder.
     *
     * This parses to a {@link DateTimeBuilder} ensuring that the text is fully parsed.
     * This method throws {@link DateTimeParseException} if unable to parse, or
     * some other {@link DateTimeException} if another date/time problem occurs.
     *
     * @param text  the text to parse, not null
     * @param position  the position to parse from, updated with length parsed
     *  and the index of any error, null if parsing whole string
     * @return the engine representing the result of the parse, not null
     * @throws DateTimeParseException if the parse fails
     */
    _parseToBuilder(text, position) {
        const pos = (position != null ? position : new ParsePosition(0));
        const result = this._parseUnresolved0(text, pos);
        if (result == null || pos.getErrorIndex() >= 0 || (position == null && pos.getIndex() < text.length)) {
            let abbr = '';
            if (text.length > 64) {
                abbr = `${text.substr(0, 64).toString()}...`;
            } else {
                abbr = text;
            }
            if (pos.getErrorIndex() >= 0) {
                throw new DateTimeParseException(`Text '${abbr}' could not be parsed at index ${ 
                    pos.getErrorIndex()}`, text, pos.getErrorIndex());
            } else {
                throw new DateTimeParseException(`Text '${abbr}' could not be parsed, unparsed text found at index ${ 
                    pos.getIndex()}`, text, pos.getIndex());
            }
        }
        return result.toBuilder();
    }

    /**
     * Parses the text using this formatter, without resolving the result, intended
     * for advanced use cases.
     *
     * Parsing is implemented as a two-phase operation.
     * First, the text is parsed using the layout defined by the formatter, producing
     * a {@link Map} of field to value, a {@link ZoneId} and a {@link Chronology}.
     * Second, the parsed data is *resolved*, by validating, combining and
     * simplifying the various fields into more useful ones.
     * This method performs the parsing stage but not the resolving stage.
     *
     * The result of this method is {@link TemporalAccessor} which represents the
     * data as seen in the input. Values are not validated, thus parsing a date string
     * of '2012-00-65' would result in a temporal with three fields - year of '2012',
     * month of '0' and day-of-month of '65'.
     *
     * The text will be parsed from the specified start {@link ParsePosition}.
     * The entire length of the text does not have to be parsed, the {@link ParsePosition}
     * will be updated with the index at the end of parsing.
     *
     * Errors are returned using the error index field of the {@link ParsePosition}
     * instead of {@link DateTimeParseException}.
     * The returned error index will be set to an index indicative of the error.
     * Callers must check for errors before using the context.
     *
     * If the formatter parses the same field more than once with different values,
     * the result will be an error.
     *
     * This method is intended for advanced use cases that need access to the
     * internal state during parsing. Typical application code should use
     * {@link parse} or the parse method on the target type.
     *
     * @param text  the text to parse, not null
     * @param position  the position to parse from, updated with length parsed
     *  and the index of any error, not null
     * @return the parsed text, null if the parse results in an error
     * @throws DateTimeException if some problem occurs during parsing
     * @throws IndexOutOfBoundsException if the position is invalid
     */
    parseUnresolved(text, position) {
        return this._parseUnresolved0(text, position);
    }

    _parseUnresolved0(text, position) {
        assert(text != null, 'text', NullPointerException);
        assert(position != null, 'position', NullPointerException);
        const context = new DateTimeParseContext(this);
        let pos = position.getIndex();
        pos = this._printerParser.parse(context, text, pos);
        if (pos < 0) {
            position.setErrorIndex(~pos);  // index not updated from input
            return null;
        }
        position.setIndex(pos);  // errorIndex not updated from input
        return context.toParsed();
    }

    /**
     * Returns the formatter as a composite printer parser.
     *
     * @param {boolean} optional  whether the printer/parser should be optional
     * @return {CompositePrinterParser} the printer/parser, not null
     */
    _toPrinterParser(optional) {
        return this._printerParser.withOptional(optional);
    }

    /**
     *
     * @returns {string}
     */
    toString() {
        const pattern = this._printerParser.toString();
        return pattern.indexOf('[') === 0 ? pattern : pattern.substring(1, pattern.length - 1);
    }

}

export function _init() {

    DateTimeFormatter.ISO_LOCAL_DATE = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-')
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .appendLiteral('-')
        .appendValue(ChronoField.DAY_OF_MONTH, 2)
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_LOCAL_TIME = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendLiteral(':')
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .optionalStart()
        .appendLiteral(':')
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
        .toFormatter(ResolverStyle.STRICT);

    DateTimeFormatter.ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_DATE)
        .appendLiteral('T')
        .append(DateTimeFormatter.ISO_LOCAL_TIME)
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_INSTANT = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendInstant()
        .toFormatter(ResolverStyle.STRICT);

    DateTimeFormatter.ISO_OFFSET_DATE_TIME = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        .appendOffsetId()
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_ZONED_DATE_TIME = new DateTimeFormatterBuilder()
        .append(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
        .optionalStart()
        .appendLiteral('[')
        .parseCaseSensitive()
        .appendZoneId()
        // .appendZoneRegionId()
        .appendLiteral(']')
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.BASIC_ISO_DATE = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .appendValue(ChronoField.DAY_OF_MONTH, 2)
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_OFFSET_DATE = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_DATE)
        .appendOffsetId()
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_OFFSET_TIME = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_TIME)
        .appendOffsetId()
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_ORDINAL_DATE = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-')
        .appendValue(ChronoField.DAY_OF_YEAR)
        .toFormatter(ResolverStyle.STRICT);

    DateTimeFormatter.ISO_WEEK_DATE = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
        .appendLiteral('-W')
        .appendValue(ChronoField.ALIGNED_WEEK_OF_YEAR)
        .appendLiteral('-')
        .appendValue(ChronoField.DAY_OF_WEEK)
        .toFormatter(ResolverStyle.STRICT);

    DateTimeFormatter.ISO_DATE = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_DATE)
        .optionalStart()
        .appendOffsetId()
        .optionalEnd()
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    DateTimeFormatter.ISO_TIME = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_TIME)
        .optionalStart()
        .appendOffsetId()
        .optionalEnd()
        .toFormatter(ResolverStyle.STRICT);

    DateTimeFormatter.ISO_DATE_TIME = new DateTimeFormatterBuilder()
        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        .optionalStart()
        .appendOffsetId()
        .optionalEnd()
        .toFormatter(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);

    // TODO:
    //  RFC_1123_DATE_TIME - https://www.threeten.org/threetenbp/apidocs/org/threeten/bp/format/DateTimeFormatter.html#RFC_1123_DATE_TIME

    DateTimeFormatter.PARSED_EXCESS_DAYS = createTemporalQuery('PARSED_EXCESS_DAYS', (temporal) => {
        if (temporal instanceof DateTimeBuilder) {
            return temporal.excessDays;
        } else {
            return Period.ZERO;
        }
    });

    DateTimeFormatter.PARSED_LEAP_SECOND = createTemporalQuery('PARSED_LEAP_SECOND', (temporal) => {
        if (temporal instanceof DateTimeBuilder) {
            return temporal.leapSecond;
        } else {
            return false;
        }
    });


}