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([]);