zul/src/main/resources/web/js/zul/db/datefmt.ts

Summary

Maintainability
F
2 wks
Test Coverage
/* datefmt.ts

    Purpose:

    Description:

    History:
        Fri Jan 16 19:13:43     2009, Created by Flyworld

Copyright (C) 2008 Potix Corporation. All Rights Reserved.

This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
function _parseTextToArray(txt: string, fmt: string): string[] | undefined {
    //ZK-5423
    var literals = extractLiteral(fmt); //extract literal token from format
    //remove literal from format and text
    fmt = fmt.replace(/'.*?'/g, ' ');  //remove any string enclosed by single quotes
    txt = removeLiteral(txt, literals);
    if (fmt.includes('\'')) //Bug ZK-1341: 'long+medium' format with single quote in zh_TW locale failed to parse AM/PM
        fmt = fmt.replace(/'/g, '');
    var ts: string[] = [],
        mindex = fmt.indexOf('MMM'),
        eindex = fmt.indexOf('E'),
        fmtlen = fmt.length,
        ary: string[] = [],
        //mmindex = mindex + 3,
        aa = fmt.indexOf('a'),
        gg = fmt.indexOf('G'),
        tlen = txt.replace(/[^.]/g, '').length,
        flen = fmt.replace(/[^.]/g, '').length;


    for (var i = 0, k = 0, j = txt.length; k < j; i++, k++) {
        var c = txt.charAt(k),
            f = fmtlen > i ? fmt.charAt(i) : '';
        if (c.match(/\d/)) {
            ary.push(c);
        } else if ((mindex >= 0 && mindex <= i /*&& mmindex >= i location French will lose last char */)
            || (eindex >= 0 && eindex <= i) || (aa > -1 && aa <= i) || (gg > -1 && gg <= i)) {
            if (c.match(/\w/)) {
                ary.push(c);
            } else {
                if (c.charCodeAt(0) < 128 && (c.charCodeAt(0) != 46
                    || tlen == flen || f.charCodeAt(0) == 46)) {
                    if (ary.length) {
                        ts.push(ary.join(''));
                        ary = [];
                    }
                } else
                    ary.push(c);
            }
        } else if (ary.length) {
            if (txt.charAt(k - 1).match(/\d/))
                while (f == fmt.charAt(i - 1) && f) {
                    f = fmt.charAt(++i);
                }
            ts.push(ary.join(''));
            ary = [];
        } else if (c.match(/\w/))
            return; //failed
    }
    if (ary.length) ts.push(ary.join(''));
    return ts;
}
function _parseToken(token: string, ts: string[], i: number, len: number): string {
    if (len < 2) len = 2;
    if (token && token.length > len) {
        ts[i] = token.substring(len);
        return token.substring(0, len);
    }
    return token;
}
function _parseInt(v: string): number {
    return parseInt(v, 10);
}
/* extracts any text enclosed by single quote from a string */
function extractLiteral(str: string): string[] {
    var pattern = /'(.*?)'/g,
        matches = [],
        match: RegExpExecArray | unknown;
    while ((match = pattern.exec(str)) != undefined && (match as RegExpExecArray)[1].length > 0) {
        matches.push((match as RegExpExecArray)[1] as never);
    }
    return matches;
}
function removeLiteral(str: string, literals: string[]): string {
    if (literals.length === 0) {
        return str;
    }
    var pattern = new RegExp(literals.join('|'), 'g');
    return str.replace(pattern, ' ');
}
function _digitFixed(val: string | number, digits?: number): string {
    var s = String(val);
    for (var j = digits! - s.length; --j >= 0;)
        s = '0' + s;
    return s;
}
function _dayInYear(d: DateImpl, ref: DateImpl): number {
    return Math.round((+Dates.newInstance([d.getFullYear(), d.getMonth(), d.getDate()], d.getTimeZone()) - +ref) / 864e5);
}
// Converts milli-second to day.
//    function _ms2day(t) {
//        return Math.round(t / 86400000);
//    }
// Day in year (starting at 1).
function dayInYear(d: DateImpl): string {
    var ref = Dates.newInstance([d.getFullYear(), 0, 1], d.getTimeZone());
    return _digitFixed(1 + _dayInYear(d, ref));
}
//Day in month (starting at 1).
function dayInMonth(d: DateImpl): number {
    return d.getDate();
}
//Week in month (starting at 1).
function weekInMonth(d: DateImpl, firstDayOfWeek: number): string {
    var ref = Dates.newInstance([d.getFullYear(), d.getMonth(), 1], d.getTimeZone()),
        day = ref.getDay(),
        shift = (firstDayOfWeek > day ? day + 7 : day) - firstDayOfWeek;
    return _digitFixed(1 + Math.floor((_dayInYear(d, ref) + shift) / 7));
}
//Day of week in month.
function dayOfWeekInMonth(d: DateImpl): string {
    var ref = Dates.newInstance([d.getFullYear(), d.getMonth(), 1], d.getTimeZone());
    return _digitFixed(1 + Math.floor(_dayInYear(d, ref) / 7));
}

// a proxy of Date object for leap day on Thai locale - B60-ZK-1010
@zk.WrapClass('zul.fmt.LeapDay')
class LeapDay extends zk.Object {
    /** @internal */
    _date: DateImpl;
    /** @internal */
    _offset?: number;

    constructor(date: DateImpl)
    constructor(y: number, m: number, d: number, hr: number, min: number, sec: number, msec: number, tz?: string)
    constructor(y: DateImpl | number, m?, d?, hr?, min?, sec?, msec?, tz?: string) {
        super();
        if (arguments.length > 1) {
            this._date = Dates.newInstance([y as number, m, d, hr, min, sec, msec], tz);
        } else
            this._date = y as DateImpl;
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setOffset(offset: number): void {
        this._offset = offset;
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setFullYear(fullYear: number): void {
        // no need to subtract the._offset, the caller will handle
        this._date.setFullYear(fullYear);
    }
    getFullYear(): number {
        return this._date.getFullYear() + (this._offset || 0);
    }
    getDate(): number {
        return this._date.getDate();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setDate(date: number): void {
        this._date.setDate(date);
    }
    getDay(): number {
        return this._date.getDay();
    }
    getMonth(): number {
        return this._date.getMonth();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setMonth(month: number): void {
        this._date.setMonth(month);
    }
    getHours(): number {
        return this._date.getHours();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setHours(hours: number): void {
        this._date.setHours(hours);
    }
    getMinutes(): number {
        return this._date.getMinutes();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setMinutes(minutes: number): void {
        this._date.setMinutes(minutes);
    }
    getSeconds(): number {
        return this._date.getSeconds();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setSeconds(seconds: number): void {
        this._date.setSeconds(seconds);
    }
    getMilliseconds(): number {
        return this._date.getMilliseconds();
    }
    // eslint-disable-next-line zk/javaStyleSetterSignature
    setMilliseconds(milliseconds: number): void {
        this._date.setMilliseconds(milliseconds);
    }
    getTimezoneOffset(): number {
        return this._date.getTimezoneOffset();
    }
    getRealDate(): DateImpl {
        return this._date;
    }
    getTimeZone(): string {
        return this._date.getTimeZone();
    }
}
var DateFmt = {
    _isoDateTimeFormat: new Intl.DateTimeFormat('en-US', { year: 'numeric' }),
    // strictDate: No invalid date allowed (e.g., Jan 0 or Nov 31)
    // nonLenient: strictDate + inputs must match this object's format (no additional character allowed)
    parseDate(
        txt: string,
        fmt: string,
        nonLenient?: boolean,
        refval?: DateImpl,
        localizedSymbols?: zk.LocalizedSymbols,
        tz?: string,
        strictDate?: boolean
    ): DateImpl | undefined {
        if (!fmt) fmt = 'yyyy/MM/dd';
        refval ??= zUtl.today(fmt, tz);

        localizedSymbols ??= {
            DOW_1ST: zk.DOW_1ST,
            MINDAYS: zk.MINDAYS,
            ERA: zk.ERA,
            YDELTA: zk.YDELTA,
            LAN_TAG: zk.LAN_TAG,
            SDOW: zk.SDOW,
            S2DOW: zk.S2DOW,
            FDOW: zk.FDOW,
            SMON: zk.SMON,
            S2MON: zk.S2MON,
            FMON: zk.FMON,
            APM: zk.APM
        };
        var y = refval.getFullYear(),
            m = refval.getMonth(),
            d = refval.getDate(), dFound,
            hr = refval.getHours(),
            min = refval.getMinutes(),
            sec = refval.getSeconds(),
            msec = refval.getMilliseconds(),
            aa = fmt.indexOf('a'),
            hasAM = aa > -1,
            hasG = fmt.includes('G'),
            hasHour1 = hasAM && (fmt.includes('h') || fmt.includes('K')),
            isAM,
            ts = _parseTextToArray(txt, fmt),
            regexp = /.*\D.*/,
            // ZK-2026: Don't use isNaN(), it will treat float as number.
            isNumber = !regexp.test(txt),
            eras = localizedSymbols.ERAS,
            era: zk.LocalizedSymbols.ErasElementType | undefined,
            eraKey: string | undefined;

        if (hasG && txt && eras) { // ZK-4745: parsing era for specific calendar system
            eraKey = this._findEraKey(txt, eras);
            if (eraKey)
                era = eras[eraKey];
        }

        var refDate = refval._moment.toDate(),
            localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, { year: 'numeric' }),
            eraName = localizedSymbols.ERA || (eraKey ? eraKey : this.getEraName(refDate, localizedSymbols, localeDateTimeFormat)),
            ydelta = localizedSymbols.YDELTA || (era ? (0 - era.firstYear + era.direction * 1) : this.getYDelta(refDate, localeDateTimeFormat));

        if (!ts || !ts.length) return;
        for (var i = 0, j = 0, offs = 0, fl = fmt.length; j < fl; ++j) {
            var cc = fmt.charAt(j);
            if ((cc >= 'a' && cc <= 'z') || (cc >= 'A' && cc <= 'Z')) {
                var len = 1, k: number;
                for (k = j; ++k < fl; ++len)
                    if (fmt.charAt(k) != cc)
                        break;

                var nosep, nv: number; //no separator
                if (k < fl) {
                    var c2 = fmt.charAt(k);
                    nosep = 'yuMdEmsShHkKaAG'.includes(c2);
                }
                var token = isNumber ? ts[0].substring(j - offs, k - offs) : ts[i++];
                switch (cc) {
                    case 'u':
                    case 'y':
                        // ZK-1985: Determine if token's length is less than the expected when nonLenient is true.
                        if (nonLenient && token && (token.length < len))
                            return;

                        if (nosep) {
                            if (len < 3) len = 2;
                            if (token && token.length > len) {
                                ts[--i] = token.substring(len);
                                token = token.substring(0, len);
                            }
                        }

                        // ZK-1985:    Determine if token contains non-digital word when nonLenient is true.
                        if (nonLenient && token && regexp.test(token))
                            return;

                        if (!isNaN(nv = _parseInt(token))) {
                            var newY = Math.min(nv, 200000); // Bug B50-3288904: js year limit
                            if (newY < 100 && newY === (y + ydelta) % 100) break; // assume yy is not modified
                            if (ydelta === 0 && newY < 100) { // only handle twoDigitYearStart with ISO calendar for now
                                // ZK-4235: Datefmt parseDate always return date between 1930-2029 when using yy format
                                var twoDigitYearStart = zk.TDYS,
                                    lowerBoundary = (Math.floor(twoDigitYearStart / 100) * 100) + newY,
                                    upperBoundary = lowerBoundary + 100;
                                y = lowerBoundary > twoDigitYearStart ? lowerBoundary : upperBoundary;
                            } else {
                                if (cc == 'y') {
                                    if (era)
                                        newY = era.firstYear + era.direction * (newY - 1);
                                    else
                                        newY = (y + ydelta > 0) ? newY - ydelta : 1 - newY - ydelta;
                                }
                                y = newY;
                            }
                        }
                        break;
                    case 'M':
                        var mon = token ? token.toLowerCase() : '',
                            isNumber0 = !isNaN(token as unknown as number) || len <= 2; // could be MM with nosep token
                        if (!mon) break;
                        if (!isNumber0 && token) {
                            // MMM or MMMM
                            var symbols: string[];
                            if (len == 3)
                                symbols = localizedSymbols.SMON!;
                            else if (len == 4)
                                symbols = localizedSymbols.FMON!;
                            else
                                break;

                            for (var index = symbols.length, brkswch; --index >= 0;) {
                                var monStr = symbols[index].toLowerCase();

                                if ((nonLenient && mon == monStr) || (!nonLenient && mon.startsWith(monStr))) {
                                    var monStrLen = monStr.length;

                                    if (token && token.length > monStrLen)
                                        ts[--i] = token.substring(monStrLen);

                                    m = index;
                                    brkswch = true;
                                    break; // shall break to switch level: B50-3314513
                                }
                            }
                            if (brkswch)
                                break;
                        }
                        if (len == 3 && token) {
                            if (nosep)
                                token = _parseToken(token, ts, --i, token.length);//token.length: the length of French month is 4
                            if (isNaN(nv = _parseInt(token)))
                                return; // failed, B50-3314513
                            m = nv - 1;
                        } else if (len <= 2) {
                            if (nosep && token && token.length > 2) {//Bug 2560497 : if no separator, token must be assigned.
                                ts[--i] = token.substring(2);
                                token = token.substring(0, 2);
                            }
                            if (isNaN(nv = _parseInt(token)))
                                return; // failed, B50-3314513
                            m = nv - 1;
                        } else {
                            for (var l = 0; ; ++l) {
                                if (l == 12) return; //failed
                                if (len == 3) {
                                    if (localizedSymbols.SMON![l] == token) {
                                        m = l;
                                        break;
                                    }
                                } else {
                                    if (token && localizedSymbols.FMON![l].startsWith(token)) {
                                        m = l;
                                        break;
                                    }
                                }
                            }
                        }
                        if (m > 11 /*|| m < 0 accept 0 since it is a common-yet-acceptable error*/) //restrict since user might input year for month
                            return;//failed
                        break;
                    case 'E':
                        if (nosep)
                            _parseToken(token, ts, --i, len);
                        break;
                    case 'd':
                        // ZK-1985:    Determine if token's length is less than expected when nonLenient is true.
                        if (nonLenient && token && (token.length < len))
                            return;

                        if (nosep)
                            token = _parseToken(token, ts, --i, len);

                        // ZK-1985:    Determine if token contains non-digital word when nonLenient is true.
                        if (nonLenient && token && regexp.test(token))
                            return;

                        if (!isNaN(nv = _parseInt(token))) {
                            d = nv;
                            dFound = true;
                            if (d < 0 || d > 31) //restrict since user might input year for day (ok to allow 0 and 31 for easy entry)
                                return; //failed
                        }
                        break;
                    case 'H':
                    case 'h':
                    case 'K':
                    case 'k':
                        // ZK-1985:    Determine if token's length is less than the expected when nonLenient is true.
                        if (nonLenient && token && (token.length < len))
                            return;

                        if (hasHour1 ? (cc == 'H' || cc == 'k') : (cc == 'h' || cc == 'K'))
                            break;
                        if (nosep)
                            token = _parseToken(token, ts, --i, len);

                        // ZK-1985:    Determine if token contains non-digital word when nonLenient is true.
                        if (nonLenient && token && regexp.test(token))
                            return;

                        if (!isNaN(nv = _parseInt(token)))
                            hr = (cc == 'h' && nv == 12) || (cc == 'k' && nv == 24) ?
                                0 : cc == 'K' ? nv % 12 : nv;
                        break;
                    case 'm':
                    case 's':
                    case 'S':
                        // ZK-1985:    Determine if token's length is less than the expected when nonLenient is true.
                        if (nonLenient && token && (token.length < len))
                            return;

                        if (nosep)
                            token = _parseToken(token, ts, --i, len);

                        // ZK-1985:    Determine if token contains non-digital word when nonLenient is true.
                        if (nonLenient && token && regexp.test(token))
                            return;

                        if (!isNaN(nv = _parseInt(token))) {
                            if (cc == 'm') min = nv;
                            else if (cc == 's') sec = nv;
                            else msec = nv;
                        }
                        break;
                    case 'a':
                        if (!hasHour1)
                            break;
                        if (nosep)
                            token = _parseToken(token, ts, --i, len);
                        if (!token) return; //failed
                        var tokenUpperCase = token.toUpperCase(),
                            apmText = localizedSymbols.APM![0].toUpperCase();
                        if (tokenUpperCase.startsWith(apmText)) {
                            isAM = true;
                        } else if (apmText.startsWith(tokenUpperCase) && ts[i] && apmText.endsWith(ts[i].toUpperCase())) { // ZK-5216, Spanish AM/PM
                            isAM = true;
                            i++;
                        } else {
                            isAM = false;
                        }
                        break;
                    case 'G':
                        if (nosep && eras) {
                            if (!eraName) return; // no era match
                            token = _parseToken(token, ts, --i, eraName.length);
                        }
                        if (eraName && token != eraName && eraName.length > token.length) { // there is space in eraName
                            token = eraName;
                            i += eraName.match(/([\s]+)/g)!.length;
                        }
                        break;
                    //default: ignored
                }
                j = k - 1;
            } else offs++;
        }

        if (hasHour1 && isAM === false)
            hr += 12;
        var dt: DateImpl | LeapDay; // TODO: enforce common interface?
        if (m == 1 && d == 29 && ydelta) {
            dt = new LeapDay(y, m, d, hr, min, sec, msec, tz);
            dt.setOffset(ydelta);
        } else {
            dt = Dates.newInstance([y, m, d, hr, min, sec, msec], tz);
        }
        if (!dFound && dt.getMonth() != m)
            dt = Dates.newInstance([y, m + 1, 0, hr, min, sec, msec], tz); //last date of m
        if (nonLenient || strictDate)
            if (dt.getFullYear() != y || dt.getMonth() != m || dt.getDate() != d
                || dt.getHours() != hr || dt.getMinutes() != min || dt.getSeconds() != sec) //ignore msec (safer though not accurate)
                return; //failed
        if (nonLenient) {
            txt = ts.reduce((txt, t) => txt.replace(t, ''), txt.trim());
            for (var k = txt.length; k--;) {
                var cc = txt.charAt(k);
                if ((cc > '9' || cc < '0') && !fmt.includes(cc))
                    return; //failed
            }
        }
        return +dt == +refval ? refval : dt as DateImpl;
        //we have to use getTime() since dt == refVal always false
    },
    formatDate(val: DateImpl, fmt?: string, localizedSymbols?: zk.LocalizedSymbols): string {
        if (!fmt) fmt = 'yyyy/MM/dd';

        localizedSymbols ??= {
            DOW_1ST: zk.DOW_1ST,
            MINDAYS: zk.MINDAYS,
            ERA: zk.ERA,
            YDELTA: zk.YDELTA,
            LAN_TAG: zk.LAN_TAG,
            SDOW: zk.SDOW,
            S2DOW: zk.S2DOW,
            FDOW: zk.FDOW,
            SMON: zk.SMON,
            S2MON: zk.S2MON,
            FMON: zk.FMON,
            APM: zk.APM
        };
        var txt = '',
            localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, { year: 'numeric' }),
            singleQuote;
        for (var j = 0, fl = fmt.length; j < fl; ++j) {
            var cc = fmt.charAt(j);
            if ((cc >= 'a' && cc <= 'z') || (cc >= 'A' && cc <= 'Z')) {
                if (singleQuote) {
                    txt += cc;
                    continue;
                }
                var len = 1, k: number;
                for (k = j; ++k < fl; ++len)
                    if (fmt.charAt(k) != cc) {
                        if (fmt.charAt(k) == 'r') k++; // ZK-2964: for nl local uur
                        break;
                    }

                switch (cc) {
                    case 'y':
                    case 'u':
                        var ydelta = 0;
                        if (cc == 'y') ydelta = localizedSymbols.YDELTA || this.getYDelta(val._moment.toDate(), localeDateTimeFormat);
                        var y = val.getFullYear() + ydelta;
                        // ZK-4851: the year is truncated to 2 digits only if the number of pattern letters is 2
                        if (len == 2) txt += _digitFixed(y % Math.pow(10, len), len);
                        else txt += _digitFixed(y, len);
                        break;
                    case 'M':
                        if (len <= 2) txt += _digitFixed(val.getMonth() + 1, len);
                        else if (len == 3) txt += localizedSymbols.SMON![val.getMonth()];
                        else txt += localizedSymbols.FMON![val.getMonth()];
                        break;
                    case 'd':
                        txt += _digitFixed(dayInMonth(val), len);
                        break;
                    case 'E':
                        if (len <= 3) txt += localizedSymbols.SDOW![(val.getDay() - localizedSymbols.DOW_1ST! + 7) % 7];
                        else txt += localizedSymbols.FDOW![(val.getDay() - localizedSymbols.DOW_1ST! + 7) % 7];
                        break;
                    case 'D':
                        txt += dayInYear(val);
                        break;
                    case 'w':
                        txt += zUtl.getWeekOfYear(val.getFullYear(), val.getMonth(), val.getDate(), localizedSymbols.DOW_1ST!, localizedSymbols.MINDAYS!);
                        break;
                    case 'W':
                        txt += weekInMonth(val, localizedSymbols.DOW_1ST!);
                        break;
                    case 'G':
                        txt += localizedSymbols.ERA || this.getEraName(val._moment.toDate(), localizedSymbols, localeDateTimeFormat);
                        break;
                    case 'F':
                        txt += dayOfWeekInMonth(val);
                        break;
                    case 'H':
                        if (len <= 2) txt += _digitFixed(val.getHours(), len);
                        break;
                    case 'k':
                        var h: number | string = val.getHours();
                        if (h == 0)
                            h = '24';
                        if (len <= 2) txt += _digitFixed(h, len);
                        break;
                    case 'K':
                        if (len <= 2) txt += _digitFixed(val.getHours() % 12, len);
                        break;
                    case 'h':
                        var h: number | string = val.getHours();
                        h %= 12;
                        if (h == 0)
                            h = '12';
                        if (len <= 2) txt += _digitFixed(h, len);
                        break;
                    case 'm':
                        if (len <= 2) txt += _digitFixed(val.getMinutes(), len);
                        break;
                    case 's':
                        if (len <= 2) txt += _digitFixed(val.getSeconds(), len);
                        break;
                    case 'S':
                        if (len <= 3) txt += _digitFixed(val.getMilliseconds(), len);
                        break;
                    case 'Z':
                        txt += -(val.getTimezoneOffset() / 60);
                        break;
                    case 'z':
                        txt += -(val.getTimezoneOffset() / 60);
                        break;
                    case 'a':
                        txt += localizedSymbols.APM![val.getHours() > 11 ? 1 : 0];
                        break;
                    default:
                        txt += '1';
                    //fake; SimpleDateFormat.parse might ignore it
                    //However, it must be an error if we don't generate a digit
                }
                j = k - 1;
            } else if (cc != "'") {
                txt += cc;
            } else if (cc === "'") {
                singleQuote = !singleQuote;
            }
        }
        return txt;
    },
    getYDelta(date?: Date, localeDateTimeFormat?: Intl.DateTimeFormat): number { // override
        return 0;
    },
    getEraName(date: Date, localizedSymbols: zk.LocalizedSymbols, localeDateTimeFormat?: Intl.DateTimeFormat): string { // override
        var langTag = localizedSymbols.LAN_TAG!.split('-').slice(0, 2).join('-'),
            localeDateString = date.toLocaleDateString(langTag, { era: 'short', day: 'numeric' });
        return localeDateString.split(' ')[0];
    },
    /** @internal */
    _findEraKey(txt: string, eras: NonNullable<zk.LocalizedSymbols['ERAS']>): string | undefined { // override
        return undefined;
    }
};
export { DateFmt as Date };
zk.fmt.Date = DateFmt;
/**
 * The `calendar` object provides a way
 * to convert between a specific instant in time for locale-sensitive
 * like buddhist's time.
 * <p>By default the year offset is specified from server if any.</p>
 * @since 5.0.1
 */
@zk.WrapClass('zk.fmt.Calendar')
export class Calendar extends zk.Object {
    /** @internal */
    _offset = zk.YDELTA;
    /** @internal */
    _date?: DateImpl;

    constructor(date?: DateImpl, localizedSymbols?: zk.LocalizedSymbols) {
        super();
        this._date = date;
        if (localizedSymbols) {
            const localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, {year: 'numeric'});
            this._offset = localizedSymbols.YDELTA || zk.fmt.Date.getYDelta(date ? date._moment.toDate() : undefined, localeDateTimeFormat);
        }
    }

    getTime(): DateImpl | undefined {
        return this._date;
    }

    setTime(time: DateImpl): this {
        this._date = time;
        return this;
    }

    setYearOffset(yearOffset: number): this {
        this._offset = yearOffset;
        return this;
    }

    getYearOffset(): number {
        return this._offset;
    }

    formatDate(val: DateImpl, fmt?: string, localizedSymbols?: zk.LocalizedSymbols): string {
        var d: LeapDay | undefined;
        if (localizedSymbols) {
            var localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, { year: 'numeric' });
            this._offset = localizedSymbols.YDELTA || zk.fmt.Date.getYDelta(val._moment.toDate(), localeDateTimeFormat);
        }

        if (this._offset) {
            if (val.getMonth() == 1 && val.getDate() == 29) {
                d = new LeapDay(val); // a proxy of Date
                d.setOffset(this._offset);
            }
        }
        return zk.fmt.Date.formatDate((d ?? val) as DateImpl, fmt, localizedSymbols);
    }

    toUTCDate(): DateImpl | undefined {
        if (this._date instanceof LeapDay)
            return this._date.getRealDate();
        var d: DateImpl | undefined;
        if ((d = this._date) && this._offset)
            d = Dates.newInstance(d);
        return d;
    }

    parseDate(
        txt: string,
        fmt: string,
        strict?: boolean,
        refval?: DateImpl,
        localizedSymbols?: zk.LocalizedSymbols,
        tz?: string,
        strictDate?: boolean
    ): DateImpl | undefined {
        var d = zk.fmt.Date.parseDate(txt, fmt, strict, refval, localizedSymbols, tz, strictDate);
        if (localizedSymbols) {
            var localeDateTimeFormat = new Intl.DateTimeFormat(localizedSymbols.LAN_TAG, { year: 'numeric' });
            this._offset = localizedSymbols.YDELTA || d ? zk.fmt.Date.getYDelta(d!._moment.toDate(), localeDateTimeFormat) : 0;
        }

        if (this._offset && fmt) {
            if (d instanceof LeapDay) {
                return d.getRealDate();
            }
        }
        return d;
    }

    getYear(): number {
        return this._date!.getFullYear();
    }

    // B70-ZK-2382: in Daylight Saving Time (DST), choose the last time at the end of this mechanism, it will display previous day.
    // e.g. 2014/10/19 at Brasilia (UTC-03:00), it will show 2014/10/18 23:00:00
    // so we need to increase a hour.
    escapeDSTConflict(val?: DateImpl, tz?: string): DateImpl | undefined {
        if (!val) return;
        var newVal = Dates.newInstance(val.getTime() + 3600000, tz); //plus 60*60*1000
        return newVal.getHours() != ((val.getHours() + 1) % 24) ? newVal : val;
    }
}