Home Reference Source Repository

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.
 */
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;
        }

        // FIXME not efficient but works, replace by subtree approach see threeten
        // will fail if a zoneId starts with the same substring of another zoneId
        const availableZoneIds = ZoneRulesProvider.getAvailableZoneIds();
        if (zoneIdMap.size !== availableZoneIds.length) {
            zoneIdMap = new ZoneIdMap(availableZoneIds);
        }
        let parseLength = zoneIdMap.minLength;
        const maxParseLength = Math.min(zoneIdMap.maxLength, length - position);
        while(parseLength <= maxParseLength) {
            const parsedZoneId = text.substr(position, parseLength);
            if(zoneIdMap.zoneIdMap[parsedZoneId] === true){
                context.setParsedZone(ZoneRegion.ofId(parsedZoneId));
                return position + parseLength;
            }
            parseLength += 1;
        }

        // ...
        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 ZoneIdMap {
    constructor(availableZoneIds){
        this.size = availableZoneIds.length;
        this.minLength = 0;
        this.maxLength = 0;
        this.zoneIdMap = this._createMap(availableZoneIds);
    }

    _createMap(availableZoneIds) {
        const map = {};
        for(let i = 0; i < availableZoneIds.length; i++){
            const zoneId = availableZoneIds[i];
            map[zoneId] = true;
            this._setMinMax(zoneId);
        }
        return map;
    }

    _setMinMax(zoneId) {
        if(zoneId == null) {
            return;
        }
        if (this.minLength === 0) {
            this.minLength = zoneId.length;
            this.maxLength = zoneId.length;
        } else {
            this.minLength = Math.min(this.minLength, zoneId.length);
            this.maxLength = Math.max(this.maxLength, zoneId.length);
        }
    }
}

let zoneIdMap = new ZoneIdMap([]);