Home Reference Source

packages/core/src/format/parser/ZoneIdPrinterParser.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 { ZoneOffset } from '../../ZoneOffset';
import { ZoneId } from '../../ZoneId';
import { ZoneRegion } from '../../ZoneRegion';

import { ChronoField } from '../../temporal/ChronoField';

import { ZoneRulesProvider } from '../../zone/ZoneRulesProvider';

import { OffsetIdPrinterParser } from './OffsetIdPrinterParser';

/**
 * Prints or parses a zone ID.
 * @private
 */
export class ZoneIdPrinterParser {

    /**
     *
     * @param {TemporalQuery} query
     * @param {string} description
     */
    constructor(query, description) {
        this.query = query;
        this.description = description;
    }

    //-----------------------------------------------------------------------
    /**
     *
     * @param {DateTimePrintContext } context
     * @param {StringBuilder} buf
     * @returns {boolean}
     */
    print(context, buf) {
        const zone = context.getValueQuery(this.query);
        if (zone == null) {
            return false;
        }
        buf.append(zone.id());
        return true;
    }

    //-----------------------------------------------------------------------
    /**
     * This implementation looks for the longest matching string.
     * For example, parsing Etc/GMT-2 will return Etc/GMC-2 rather than just
     * Etc/GMC although both are valid.
     *
     * This implementation uses a tree to search for valid time-zone names in
     * the parseText. The top level node of the tree has a length equal to the
     * length of the shortest time-zone as well as the beginning characters of
     * all other time-zones.
     *
     * @param {DateTimeParseContext} context
     * @param {String} text
     * @param {number} position
     * @return {number}
     */
    parse(context, text, position) {
        const length = text.length;
        if (position > length) {
            return ~position;
        }
        if (position === length) {
            return ~position;
        }

        // handle fixed time-zone IDs
        const nextChar = text.charAt(position);
        if (nextChar === '+' || nextChar === '-') {
            const newContext = context.copy();
            const endPos = OffsetIdPrinterParser.INSTANCE_ID.parse(newContext, text, position);
            if (endPos < 0) {
                return endPos;
            }
            const offset = newContext.getParsed(ChronoField.OFFSET_SECONDS);
            const zone = ZoneOffset.ofTotalSeconds(offset);
            context.setParsedZone(zone);
            return endPos;
        } else if (length >= position + 2) {
            const nextNextChar = text.charAt(position + 1);
            if (context.charEquals(nextChar, 'U') &&
                context.charEquals(nextNextChar, 'T')) {
                if (length >= position + 3 &&
                    context.charEquals(text.charAt(position + 2), 'C')) {
                    return this._parsePrefixedOffset(context, text, position, position + 3);
                }
                return this._parsePrefixedOffset(context, text, position, position + 2);
            } else if (context.charEquals(nextChar, 'G') &&
                length >= position + 3 &&
                context.charEquals(nextNextChar, 'M') &&
                context.charEquals(text.charAt(position + 2), 'T')) {
                return this._parsePrefixedOffset(context, text, position, position + 3);
            }
        }
        // javascript special case
        if(text.substr(position, 6) === 'SYSTEM'){
            context.setParsedZone(ZoneId.systemDefault());
            return position + 6;
        }

        // ...
        if (context.charEquals(nextChar, 'Z')) {
            context.setParsedZone(ZoneOffset.UTC);
            return position + 1;
        }

        const availableZoneIds = ZoneRulesProvider.getAvailableZoneIds();
        if (zoneIdTree.size !== availableZoneIds.length) {
            zoneIdTree = ZoneIdTree.createTreeMap(availableZoneIds);
        }

        const maxParseLength = length - position;
        let treeMap = zoneIdTree.treeMap;
        let parsedZoneId = null;
        let parseLength = 0;
        while(treeMap != null) {
            const parsedSubZoneId = text.substr(position, Math.min(treeMap.length, maxParseLength));
            treeMap = treeMap.get(parsedSubZoneId);
            if (treeMap != null && treeMap.isLeaf) {
                parsedZoneId = parsedSubZoneId;
                parseLength = treeMap.length;
            }
        }
        if (parsedZoneId != null) {
            context.setParsedZone(ZoneRegion.ofId(parsedZoneId));
            return position + parseLength;
        }

        return ~position;
    }

    /**
     *
     * @param {DateTimeParseContext} context
     * @param {String} text
     * @param {number} prefixPos
     * @param {number} position
     * @return {number}
     */
    _parsePrefixedOffset(context, text, prefixPos, position) {
        const prefix = text.substring(prefixPos, position).toUpperCase();
        const newContext = context.copy();
        if (position < text.length && context.charEquals(text.charAt(position), 'Z')) {
            context.setParsedZone(ZoneId.ofOffset(prefix, ZoneOffset.UTC));
            return position;
        }
        const endPos = OffsetIdPrinterParser.INSTANCE_ID.parse(newContext, text, position);
        if (endPos < 0) {
            context.setParsedZone(ZoneId.ofOffset(prefix, ZoneOffset.UTC));
            return position;
        }
        const offsetSecs = newContext.getParsed(ChronoField.OFFSET_SECONDS);
        const offset = ZoneOffset.ofTotalSeconds(offsetSecs);
        context.setParsedZone(ZoneId.ofOffset(prefix, offset));
        return endPos;
    }

    /**
     *
     * @returns {string}
     */
    toString() {
        return this.description;
    }
}

class ZoneIdTree {

    static createTreeMap(availableZoneIds) {
        const sortedZoneIds =  availableZoneIds.sort((a, b) => a.length - b.length);
        const treeMap = new ZoneIdTreeMap(sortedZoneIds[0].length, false);
        for (let i=0; i<sortedZoneIds.length; i++){
            treeMap.add(sortedZoneIds[i]);
        }
        return new ZoneIdTree(sortedZoneIds.length, treeMap);
    }

    constructor(size, treeMap) {
        this.size = size;
        this.treeMap = treeMap;
    }
}

class ZoneIdTreeMap {
    constructor(length = 0, isLeaf = false){
        this.length = length;
        this.isLeaf = isLeaf;
        this._treeMap = {};
    }

    add(zoneId){
        const idLength = zoneId.length;
        if(idLength === this.length) {
            this._treeMap[zoneId] = new ZoneIdTreeMap(idLength, true);
        } else if (idLength > this.length) {
            const subZoneId = zoneId.substr(0, this.length);
            let subTreeMap = this._treeMap[subZoneId];
            if (subTreeMap == null) {
                subTreeMap = new ZoneIdTreeMap(idLength, false);
                this._treeMap[subZoneId] = subTreeMap;
            }
            subTreeMap.add(zoneId);
        }
    }

    get(zoneId){
        return this._treeMap[zoneId];
    }
}

let zoneIdTree = new ZoneIdTree([]);