Source

TimeZone.js

/*
 * TimeZone.js - Definition of a time zone class
 *
 * Copyright © 2012-2015, 2018, 2023 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// !data localeinfo zoneinfo

var ilib = require("../index.js");
var Utils = require("./Utils.js");
var MathUtils = require("./MathUtils.js");
var JSUtils = require("./JSUtils.js");

var Locale = require("./Locale.js");
var LocaleInfo = require("./LocaleInfo.js");

var GregRataDie = require("./GregRataDie.js");
var CalendarFactory = require("./CalendarFactory.js");
var IString = require("./IString.js");
var DateFactory = require("./DateFactory.js");

/**
 * @class
 * Create a time zone instance.
 *
 * This class reports and transforms
 * information about particular time zones.<p>
 *
 * The options parameter may contain any of the following properties:
 *
 * <ul>
 * <li><i>id</i> - The id of the requested time zone such as "Europe/London" or
 * "America/Los_Angeles". These are taken from the IANA time zone database. (See
 * http://www.iana.org/time-zones for more information.) <p>
 *
 * There is one special
 * time zone that is not taken from the IANA database called simply "local". In
 * this case, this class will attempt to discover the current time zone and
 * daylight savings time settings by calling standard Javascript classes to
 * determine the offsets from UTC.
 *
 * <li><i>locale</i> - The locale for this time zone.
 *
 * <li><i>offset</i> - Choose the time zone based on the offset from UTC given in
 * number of minutes (negative is west, positive is east).
 *
 * <li><i>onLoad</i> - a callback function to call when the data is fully
 * loaded. When the onLoad option is given, this class will attempt to
 * load any missing locale data using the ilib loader callback.
 * When the data is loaded, the onLoad function is called with the current
 * instance as a parameter.
 *
 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 * asynchronously. If this option is given as "false", then the "onLoad"
 * callback must be given, as the instance returned from this constructor will
 * not be usable for a while.
 *
 * <li><i>loadParams</i> - an object containing parameters to pass to the
 * loader callback function when locale data is missing. The parameters are not
 * interpretted or modified in any way. They are simply passed along. The object
 * may contain any property/value pairs as long as the calling code is in
 * agreement with the loader callback function as to what those parameters mean.
 * </ul>
 *
 * There is currently no way in the ECMAscript
 * standard to tell which exact time zone is currently in use. Choosing the
 * id "locale" or specifying an explicit offset will not give a specific time zone,
 * as it is impossible to tell with certainty which zone the offsets
 * match.<p>
 *
 * When the id "local" is given or the offset option is specified, this class will
 * have the following behaviours:
 * <ul>
 * <li>The display name will always be given as the RFC822 style, no matter what
 * style is requested
 * <li>The id will also be returned as the RFC822 style display name
 * <li>When the offset is explicitly given, this class will assume the time zone
 * does not support daylight savings time, and the offsets will be calculated
 * the same way year round.
 * <li>When the offset is explicitly given, the inDaylightSavings() method will
 * always return false.
 * <li>When the id "local" is given, this class will attempt to determine the
 * daylight savings time settings by examining the offset from UTC on Jan 1
 * and June 1 of the current year. If they are different, this class assumes
 * that the local time zone uses DST. When the offset for a particular date is
 * requested, it will use the built-in Javascript support to determine the
 * offset for that date.
 * </ul>
 *
 * If a more specific time zone is
 * needed with display names and known start/stop times for DST, use the "id"
 * property instead to specify the time zone exactly. You can perhaps ask the
 * user which time zone they prefer so that your app does not need to guess.<p>
 *
 * If the id and the offset are both not given, the default time zone for the
 * locale is retrieved from
 * the locale info. If the locale is not specified, the default locale for the
 * library is used.<p>
 *
 * Because this class was designed for use in web sites, and the vast majority
 * of dates and times being formatted are recent date/times, this class is simplified
 * by not implementing historical time zones. That is, when governments change the
 * time zone rules for a particular zone, only the latest such rule is implemented
 * in this class. That means that determining the offset for a date that is prior
 * to the last change may give the wrong result. Historical time zone calculations
 * may be implemented in a later version of iLib if there is enough demand for it,
 * but it would entail a much larger set of time zone data that would have to be
 * loaded.
 *
 *
 * @constructor
 * @param {Object} options Options guiding the construction of this time zone instance
 */
var TimeZone = function(options) {
    this.sync = true;
    this.locale = new Locale();
    this.isLocal = false;

    if (options) {
        if (options.locale) {
            this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
        }

        if (options.id) {
            var id = options.id.toString();
            if (id === 'local') {
                this.isLocal = true;

                // use standard Javascript Date to figure out the time zone offsets
                var now = new Date(),
                    jan1 = new Date(now.getFullYear(), 0, 1),  // months in std JS Date object are 0-based
                    jun1 = new Date(now.getFullYear(), 5, 1);

                // Javascript's method returns the offset backwards, so we have to
                // take the negative to get the correct offset
                this.offsetJan1 = -jan1.getTimezoneOffset();
                this.offsetJun1 = -jun1.getTimezoneOffset();
                // the offset of the standard time for the time zone is always the one that is closest
                // to negative infinity of the two, no matter whether you are in the northern or southern
                // hemisphere, east or west
                this.offset = Math.min(this.offsetJan1, this.offsetJun1);
            }
            this.id = id;
        } else if (options.offset) {
            this.offset = (typeof(options.offset) === 'string') ? parseInt(options.offset, 10) : options.offset;
            this.id = this.getDisplayName(undefined, undefined);
        }

        if (typeof(options.sync) !== 'undefined') {
            this.sync = !!options.sync;
        }

        this.loadParams = options.loadParams;
        this.onLoad = options.onLoad;
    }

    //console.log("timezone: locale is " + this.locale);

    if (!this.id) {
        new LocaleInfo(this.locale, {
            sync: this.sync,
            loadParams: this.loadParams,
            onLoad: ilib.bind(this, function (li) {
                this.id = li.getTimeZone() || "Etc/UTC";
                this._loadtzdata();
            })
        });
    } else {
        this._loadtzdata();
    }

    //console.log("localeinfo is: " + JSON.stringify(this.locinfo));
    //console.log("id is: " + JSON.stringify(this.id));
};

/*
 * Explanation of the compressed time zone info properties.
 * {
 *     "o": "8:0",      // offset from UTC
 *     "f": "W{c}T",    // standard abbreviation. For time zones that observe DST, the {c} replacement is replaced with the
 *                      // letter in the e.c or s.c properties below
 *     "e": {           // info about the end of DST
 *         "j": 78322.5 // Julian day when the transition happens. Either specify the "j" property or all of the "m", "r", and
 *                      // "t" properties, but not both sets.
 *         "m": 3,      // month that it ends
 *         "r": "l0",   // rule for the day it ends "l" = "last", numbers are Sun=0 through Sat=6. Other syntax is "0>7".
 *                      // This means the 0-day (Sun) after the 7th of the month. Other possible operators are <, >, <=, >=
 *         "t": "2:0",  // time of day that the DST turns off, hours:minutes
 *         "c": "S"     // character to replace into the abbreviation for standard time
 *     },
 *     "s": {           // info about the start of DST
 *         "j": 78189.5 // Julian day when the transition happens. Either specify the "j" property or all of the "m", "r", and
 *                      // "t" properties, but not both sets.
 *         "m": 10,     // month that it starts
 *         "r": "l0",   // rule for the day it starts "l" = "last", numbers are Sun=0 through Sat=6. Other syntax is "0>7".
 *                      // This means the 0-day (Sun) after the 7th of the month. Other possible operators are <, >, <=, >=
 *         "t": "2:0",  // time of day that the DST turns on, hours:minutes
 *         "v": "1:0",  // amount of time saved in hours:minutes
 *         "c": "D"     // character to replace into the abbreviation for daylight time
 *     },
 *     "c": "AU",       // ISO code for the country that contains this time zone
 *     "n": "W. Australia {c} Time"
 *                      // long English name of the zone. The {c} replacement is for the word "Standard" or "Daylight" as appropriate
 * }
 */
TimeZone.prototype._loadtzdata = function () {
    var zoneName = this.id.replace(/-/g, "m").replace(/\+/g, "p");
    // console.log("id is: " + JSON.stringify(this.id));
    // console.log("zoneinfo is: " + JSON.stringify(ilib.data.zoneinfo[zoneName]));
    if (!ilib.data.zoneinfo[zoneName] && typeof(this.offset) === 'undefined') {
        Utils.loadData({
            object: "TimeZone",
            nonlocale: true,    // locale independent
            name: "zoneinfo/" + this.id + ".json",
            sync: this.sync,
            loadParams: this.loadParams,
            callback: ilib.bind(this, function (tzdata) {
                if (tzdata && !JSUtils.isEmpty(tzdata)) {
                    ilib.data.zoneinfo[zoneName] = tzdata;
                }
                this._initZone(zoneName);
            })
        });
    } else {
        this._initZone(zoneName);
    }
};

TimeZone.prototype._initZone = function(zoneName) {
    /**
     * @private
     * @type {{o:string,f:string,e:Object.<{m:number,r:string,t:string,z:string}>,s:Object.<{m:number,r:string,t:string,z:string,v:string,c:string}>,c:string,n:string}}
     */
    this.zone = ilib.data.zoneinfo[zoneName];
    if (!this.zone && typeof(this.offset) === 'undefined') {
        this.id = "Etc/UTC";
        this.zone = ilib.data.zoneinfo[this.id];
    }

    this._calcDSTSavings();

    if (typeof(this.offset) === 'undefined' && this.zone.o) {
        var offsetParts = this._offsetStringToObj(this.zone.o);
        /**
         * raw offset from UTC without DST, in minutes
         * @private
         * @type {number}
         */
        this.offset = (Math.abs(offsetParts.h || 0) * 60 + (offsetParts.m || 0)) * MathUtils.signum(offsetParts.h || 0);
    }

    if (this.onLoad && typeof(this.onLoad) === 'function') {
        this.onLoad(this);
    }
};

/** @private */
TimeZone._marshallIds = function (country, sync, callback) {
    var tz, ids = [];

    if (!country) {
        // local is a special zone meaning "the local time zone according to the JS engine we are running upon"
        ids.push("local");
        for (tz in ilib.data.timezones) {
            if (ilib.data.timezones[tz]) {
                ids.push(ilib.data.timezones[tz]);
            }
        }
        if (typeof(callback) === 'function') {
            callback(ids);
        }
    } else {
        if (!ilib.data.zoneinfo.zonetab) {
            Utils.loadData({
                object: "TimeZone",
                nonlocale: true,    // locale independent
                name: "zoneinfo/zonetab.json",
                sync: sync,
                callback: ilib.bind(this, function (tzdata) {
                    if (tzdata) {
                        ilib.data.zoneinfo.zonetab = tzdata;
                    }

                    ids = ilib.data.zoneinfo.zonetab[country];

                    if (typeof(callback) === 'function') {
                        callback(ids);
                    }
                })
            });
        } else {
            ids = ilib.data.zoneinfo.zonetab[country];
            if (typeof(callback) === 'function') {
                callback(ids);
            }
        }
    }

    return ids;
};

/**
 * Return an array of available zone ids that the constructor knows about.
 * The country parameter is optional. If it is not given, all time zones will
 * be returned. If it specifies a country code, then only time zones for that
 * country will be returned.
 *
 * @param {string|undefined} country country code for which time zones are being sought
 * @param {boolean} sync whether to find the available ids synchronously (true) or asynchronously (false)
 * @param {function(Array.<string>)} onLoad callback function to call when the data is finished loading
 * @return {Array.<string>} an array of zone id strings
 */
TimeZone.getAvailableIds = function (country, sync, onLoad) {
    var tz, ids = [];

    if (typeof(sync) !== 'boolean') {
        sync = true;
    }

    if (ilib.data.timezones.length === 0) {
        if (typeof(ilib._load) !== 'undefined' && typeof(ilib._load.listAvailableFiles) === 'function') {
            ilib._load.listAvailableFiles(sync, function(hash) {
                for (var dir in hash) {
                    var files = hash[dir];
                    if (ilib.isArray(files)) {
                        files.forEach(function (filename) {
                            if (filename && filename.match(/^zoneinfo/)) {
                                ilib.data.timezones.push(filename.replace(/^zoneinfo\//, "").replace(/\.json$/, ""));
                            }
                        });
                    }
                }
                ids = TimeZone._marshallIds(country, sync, onLoad);
            });
        } else {
            for (tz in ilib.data.zoneinfo) {
                if (ilib.data.zoneinfo[tz]) {
                    ilib.data.timezones.push(tz);
                }
            }
            ids = TimeZone._marshallIds(country, sync, onLoad);
        }
    } else {
        ids = TimeZone._marshallIds(country, sync, onLoad);
    }

    return ids;
};

/**
 * Return the id used to uniquely identify this time zone.
 * @return {string} a unique id for this time zone
 */
TimeZone.prototype.getId = function () {
    return this.id.toString();
};

/**
 * Return the abbreviation that is used for the current time zone on the given date.
 * The date may be in DST or during standard time, and many zone names have different
 * abbreviations depending on whether or not the date is falls within DST.<p>
 *
 * There are two styles that are supported:
 *
 * <ol>
 * <li>standard - returns the 3 to 5 letter abbreviation of the time zone name such
 * as "CET" for "Central European Time" or "PDT" for "Pacific Daylight Time"
 * <li>rfc822 - returns an RFC 822 style time zone specifier, which specifies more
 * explicitly what the offset is from UTC
 * <li>long - returns the long name of the zone in English
 * </ol>
 *
 * @param {IDate|Object|JulianDay|Date|string|number=} date a date to determine if it is in daylight time or standard time
 * @param {string=} style one of "standard" or "rfc822". Default if not specified is "standard"
 * @return {string} the name of the time zone, abbreviated according to the style
 */
TimeZone.prototype.getDisplayName = function (date, style) {
    var temp;
    style = (this.isLocal || typeof(this.zone) === 'undefined') ? "rfc822" : (style || "standard");
    switch (style) {
        default:
        case 'standard':
            if (this.zone.f && this.zone.f !== "zzz") {
                if (this.zone.f.indexOf("{c}") !== -1) {
                    var letter = "";
                    letter = this.inDaylightTime(date) ? this.zone.s && this.zone.s.c : this.zone.e && this.zone.e.c;
                    temp = new IString(this.zone.f);
                    return temp.format({c: letter || ""});
                }
                return this.zone.f;
            }
            temp = "GMT" + this.zone.o;
            if (this.inDaylightTime(date)) {
                temp += "+" + this.zone.s.v;
            }
            return temp;

        case 'rfc822':
            var offset = this.getOffset(date), // includes the DST if applicable
                ret = "UTC",
                hour = offset.h || 0,
                minute = offset.m || 0;

            if (hour !== 0) {
                ret += (hour > 0) ? "+" : "-";
                if (Math.abs(hour) < 10) {
                    ret += "0";
                }
                ret += (hour < 0) ? -hour : hour;
                if (minute < 10) {
                    ret += "0";
                }
                ret += minute;
            }
            return ret;

        case 'long':
            if (this.zone.n) {
                if (this.zone.n.indexOf("{c}") !== -1) {
                    var str = this.inDaylightTime(date) ? "Daylight" : "Standard";
                    temp = new IString(this.zone.n);
                    return temp.format({c: str || ""});
                }
                return this.zone.n;
            }
            temp = "GMT" + this.zone.o;
            if (this.inDaylightTime(date)) {
                temp += "+" + this.zone.s.v;
            }
            return temp;
    }
};

/**
 * Convert the offset string to an object with an h, m, and possibly s property
 * to indicate the hours, minutes, and seconds.
 *
 * @private
 * @param {string} str the offset string to convert to an object
 * @return {Object.<{h:number,m:number,s:number}>} an object giving the offset for the zone at
 * the given date/time, in hours, minutes, and seconds
 */
TimeZone.prototype._offsetStringToObj = function (str) {
    var offsetParts = (typeof(str) === 'string') ? str.split(":") : [],
        ret = {h:0},
        temp;

    if (offsetParts.length > 0) {
        ret.h = parseInt(offsetParts[0], 10);
        if (offsetParts.length > 1) {
            temp = parseInt(offsetParts[1], 10);
            if (temp) {
                ret.m = temp;
            }
            if (offsetParts.length > 2) {
                temp = parseInt(offsetParts[2], 10);
                if (temp) {
                    ret.s = temp;
                }
            }
        }
    }

    return ret;
};

/**
 * Returns the offset of this time zone from UTC at the given date/time. If daylight saving
 * time is in effect at the given date/time, this method will return the offset value
 * adjusted by the amount of daylight saving.
 * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed
 * @return {Object.<{h:number,m:number}>} an object giving the offset for the zone at
 * the given date/time, in hours, minutes, and seconds
 */
TimeZone.prototype.getOffset = function (date) {
    if (!date) {
        return this.getRawOffset();
    }
    var offset = this.getOffsetMillis(date)/60000;

    var hours = MathUtils.down(offset/60),
        minutes = Math.abs(offset) - Math.abs(hours)*60;

    var ret = {
        h: hours
    };
    if (minutes !== 0) {
        ret.m = minutes;
    }
    return ret;
};

/**
 * Returns the offset of this time zone from UTC at the given date/time expressed in
 * milliseconds. If daylight saving
 * time is in effect at the given date/time, this method will return the offset value
 * adjusted by the amount of daylight saving. Negative numbers indicate offsets west
 * of UTC and conversely, positive numbers indicate offset east of UTC.
 *
 * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed, or null for the
 * present date
 * @return {number} the number of milliseconds of offset from UTC that the given date is
 */
TimeZone.prototype.getOffsetMillis = function (date) {
    var ret;

    // check if the dst property is defined -- the intrinsic JS Date object doesn't work so
    // well if we are in the overlap time at the end of DST
    if (this.isLocal && typeof(date.dst) === 'undefined') {
        var d = (!date) ? new Date() : new Date(date.getTimeExtended());
        return -d.getTimezoneOffset() * 60000;
    }

    ret = this.offset;

    if (date && this.inDaylightTime(date)) {
        ret += this.dstSavings;
    }

    return ret * 60000;
};

/**
 * Return the offset in milliseconds when the date has an RD number in wall
 * time rather than in UTC time.
 * @private
 * @param {IDate|Object|JulianDay|Date|string|number} date the date to check in wall time
 * @returns {number} the number of milliseconds of offset from UTC that the given date is
 */
TimeZone.prototype._getOffsetMillisWallTime = function (date) {
    var ret;

    ret = this.offset;

    if (date && this.inDaylightTime(date, true)) {
        ret += this.dstSavings;
    }

    return ret * 60000;
};

/**
 * Returns the offset of this time zone from UTC at the given date/time. If daylight saving
 * time is in effect at the given date/time, this method will return the offset value
 * adjusted by the amount of daylight saving.
 * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed
 * @return {string} the offset for the zone at the given date/time as a string in the
 * format "h:m:s"
 */
TimeZone.prototype.getOffsetStr = function (date) {
    var offset = this.getOffset(date),
        ret;

    ret = offset.h;
    if (typeof(offset.m) !== 'undefined') {
        ret += ":" + offset.m;
        if (typeof(offset.s) !== 'undefined') {
            ret += ":" + offset.s;
        }
    } else {
        ret += ":0";
    }

    return ret;
};

/**
 * Gets the offset from UTC for this time zone.
 * @return {Object.<{h:number,m:number,s:number}>} an object giving the offset from
 * UTC for this time zone, in hours, minutes, and seconds
 */
TimeZone.prototype.getRawOffset = function () {
    var hours = MathUtils.down(this.offset/60),
        minutes = Math.abs(this.offset) - Math.abs(hours)*60;

    var ret = {
        h: hours
    };
    if (minutes != 0) {
        ret.m = minutes;
    }
    return ret;
};

/**
 * Gets the offset from UTC for this time zone expressed in milliseconds. Negative numbers
 * indicate zones west of UTC, and positive numbers indicate zones east of UTC.
 *
 * @return {number} an number giving the offset from
 * UTC for this time zone in milliseconds
 */
TimeZone.prototype.getRawOffsetMillis = function () {
    return this.offset * 60000;
};

/**
 * Gets the offset from UTC for this time zone without DST savings.
 * @return {string} the offset from UTC for this time zone, in the format "h:m:s"
 */
TimeZone.prototype.getRawOffsetStr = function () {
    var off = this.getRawOffset();
    return off.h + ":" + (off.m || "0");
};

/**
 * Return the amount of time in hours:minutes that the clock is advanced during
 * daylight savings time.
 * @return {Object.<{h:number,m:number,s:number}>} the amount of time that the
 * clock advances for DST in hours, minutes, and seconds
 */
TimeZone.prototype.getDSTSavings = function () {
    if (this.isLocal) {
        // take the absolute because the difference in the offsets may be positive or
        // negative, depending on the hemisphere
        var savings = Math.abs(this.offsetJan1 - this.offsetJun1);
        var hours = MathUtils.down(savings/60),
            minutes = savings - hours*60;
        return {
            h: hours,
            m: minutes
        };
    } else if (this.zone && this.zone.s) {
        return this._offsetStringToObj(this.zone.s.v);    // this.zone.start.savings
    }
    return {h:0};
};

/**
 * Return the amount of time in hours:minutes that the clock is advanced during
 * daylight savings time.
 * @return {string} the amount of time that the clock advances for DST in the
 * format "h:m:s"
 */
TimeZone.prototype.getDSTSavingsStr = function () {
    if (this.isLocal) {
        var savings = this.getDSTSavings();
        return savings.h + ":" + savings.m;
    } else if (typeof(this.offset) !== 'undefined' && this.zone && this.zone.s) {
        return this.zone.s.v;    // this.zone.start.savings
    }
    return "0:0";
};

/**
 * return the rd of the start of DST transition for the given year
 * @private
 * @param {Object} rule set of rules
 * @param {number} year year to check
 * @return {number} the rd of the start of DST for the year
 */
TimeZone.prototype._calcRuleStart = function (rule, year) {
    var type = "=",
        weekday = 0,
        day,
        refDay,
        cal,
        hour = 0,
        minute = 0,
        second = 0,
        time,
        i;

    if (typeof(rule.j) !== 'undefined') {
        refDay = new GregRataDie({
            julianday: rule.j
        });
    } else {
        if (rule.r.charAt(0) === 'l' || rule.r.charAt(0) === 'f') {
            cal = CalendarFactory({type: "gregorian"}); // can be synchronous
            type = rule.r.charAt(0);
            weekday = parseInt(rule.r.substring(1), 10);
            day = (type === 'l') ? cal.getMonLength(rule.m, year) : 1;
            //console.log("_calcRuleStart: Calculating the " +
            //        (rule.r.charAt(0) == 'f' ? "first " : "last ") + weekday +
            //        " of month " + rule.m);
        } else {
            i = rule.r.indexOf('<');
            if (i === -1) {
                i = rule.r.indexOf('>');
            }

            if (i !== -1) {
                type = rule.r.charAt(i);
                weekday = parseInt(rule.r.substring(0, i), 10);
                day = parseInt(rule.r.substring(i+1), 10);
                //console.log("_calcRuleStart: Calculating the " + weekday +
                //        type + day + " of month " + rule.m);
            } else {
                day = parseInt(rule.r, 10);
                //console.log("_calcRuleStart: Calculating the " + day + " of month " + rule.m);
            }
        }

        if (rule.t) {
            time = rule.t.split(":");
            hour = parseInt(time[0], 10);
            if (time.length > 1) {
                minute = parseInt(time[1], 10);
                if (time.length > 2) {
                    second = parseInt(time[2], 10);
                }
            }
        }
        //console.log("calculating rd of " + year + "/" + rule.m + "/" + day);
        refDay = new GregRataDie({
            year: year,
            month: rule.m,
            day: day,
            hour: hour,
            minute: minute,
            second: second
        });
    }
    //console.log("refDay is " + JSON.stringify(refDay));
    var d = refDay.getRataDie();

    switch (type) {
        case 'l':
        case '<':
            //console.log("returning " + refDay.onOrBefore(rd, weekday));
            d = refDay.onOrBefore(weekday);
            break;
        case 'f':
        case '>':
            //console.log("returning " + refDay.onOrAfterRd(rd, weekday));
            d = refDay.onOrAfter(weekday);
            break;
    }
    return d;
};

/**
 * @private
 */
TimeZone.prototype._calcDSTSavings = function () {
    var saveParts = this.getDSTSavings();

    /**
     * savings in minutes when DST is in effect
     * @private
     * @type {number}
     */
    this.dstSavings = (Math.abs(saveParts.h || 0) * 60 + (saveParts.m || 0)) * MathUtils.signum(saveParts.h || 0);
};

/**
 * @private
 */
TimeZone.prototype._getDSTStartRule = function (year) {
    // TODO: update this when historic/future zones are supported
    return this.zone.s;
};

/**
 * @private
 */
TimeZone.prototype._getDSTEndRule = function (year) {
    // TODO: update this when historic/future zones are supported
    return this.zone.e;
};

/**
 * Returns whether or not the given date is in daylight saving time for the current
 * zone. Note that daylight savings time is observed for the summer. Because
 * the seasons are reversed, daylight savings time in the southern hemisphere usually
 * runs from the end of the year through New Years into the first few months of the
 * next year. This method will correctly calculate the start and end of DST for any
 * location.
 *
 * @param {IDate|Object|JulianDay|Date|string|number=} date a date for which the info about daylight time is being sought,
 * or undefined to tell whether we are currently in daylight savings time
 * @param {boolean=} wallTime if true, then the given date is in wall time. If false or
 * undefined, it is in the usual UTC time.
 * @return {boolean} true if the given date is in DST for the current zone, and false
 * otherwise.
 */
TimeZone.prototype.inDaylightTime = function (date, wallTime) {
    var rd, startRd, endRd, year;
    if (date) {
        // need an IDate instance, so convert as necessary
        date = DateFactory._dateToIlib(date, this.id, this.locale);
    }
    if (this.isLocal) {
        // check if the dst property is defined -- the intrinsic JS Date object doesn't work so
        // well if we are in the overlap time at the end of DST, so we have to work around that
        // problem by adding in the savings ourselves
        var offset = this.offset * 60000;
        if (typeof(date.dst) !== 'undefined' && !date.dst) {
            offset += this.dstSavings * 60000;
        }

        var d = new Date(date ? date.getTimeExtended() - offset: undefined);
        // the DST offset is always the one that is closest to positive infinity, no matter
        // if you are in the northern or southern hemisphere, east or west
        var dst = Math.max(this.offsetJan1, this.offsetJun1);
        return (-d.getTimezoneOffset() === dst);
    }

    if (!date || !date.cal || date.cal.type !== "gregorian") {
        // convert to Gregorian so that we can tell if it is in DST or not
        var time = date && typeof(date.getTimeExtended) === 'function' ? date.getTimeExtended() : undefined;
        rd = new GregRataDie({unixtime: time}).getRataDie();
        year = new Date(time).getUTCFullYear();
    } else {
        rd = date.rd.getRataDie();
        year = date.year;
    }
    // rd should be a Gregorian RD number now, in UTC

    // if we aren't using daylight time in this zone for the given year, then we are
    // not in daylight time
    if (!this.useDaylightTime(year)) {
        return false;
    }

    // these calculate the start/end in local wall time
    var startrule = this._getDSTStartRule(year);
    var endrule = this._getDSTEndRule(year);
    startRd = this._calcRuleStart(startrule, year);
    endRd = this._calcRuleStart(endrule, year);

    if (wallTime) {
        // rd is in wall time, so we have to make sure to skip the missing time
        // at the start of DST when standard time ends and daylight time begins
        startRd += this.dstSavings/1440;
    } else {
        // rd is in UTC, so we have to convert the start/end to UTC time so
        // that they can be compared directly to the UTC rd number of the date

        // when DST starts, time is standard time already, so we only have
        // to subtract the offset to get to UTC and not worry about the DST savings
        startRd -= this.offset/1440;

        // when DST ends, time is in daylight time already, so we have to
        // subtract the DST savings to get back to standard time, then the
        // offset to get to UTC
        endRd -= (this.offset + this.dstSavings)/1440;
    }

    // In the northern hemisphere, the start comes first some time in spring (Feb-Apr),
    // then the end some time in the fall (Sept-Nov). In the southern
    // hemisphere, it is the other way around because the seasons are reversed. Standard
    // time is still in the winter, but the winter months are May-Aug, and daylight
    // savings time usually starts Aug-Oct of one year and runs through Mar-May of the
    // next year.
    if (rd < endRd && endRd - rd <= this.dstSavings/1440 && typeof(date.dst) === 'boolean') {
        // take care of the magic overlap time at the end of DST
        return date.dst;
    }
    if (startRd < endRd) {
        // northern hemisphere
        return (rd >= startRd && rd < endRd) ? true : false;
    }
    // southern hemisphere
    return (rd >= startRd || rd < endRd) ? true : false;
};

/**
 * Returns true if this time zone switches to daylight savings time at some point
 * in the year, and false otherwise.
 * @param {number} year Whether or not the time zone uses daylight time in the given year. If
 * this parameter is not given, the current year is assumed.
 * @return {boolean} true if the time zone uses daylight savings time
 */
TimeZone.prototype.useDaylightTime = function (year) {

    // this zone uses daylight savings time iff there is a rule defining when to start
    // and when to stop the DST
    return (this.isLocal && this.offsetJan1 !== this.offsetJun1) ||
        (typeof(this.zone) !== 'undefined' &&
        typeof(this.zone.s) !== 'undefined' &&
        typeof(this.zone.e) !== 'undefined');
};

/**
 * Returns the ISO 3166 code of the country for which this time zone is defined.
 * @return {string} the ISO 3166 code of the country for this zone
 */
TimeZone.prototype.getCountry = function () {
    return this.zone.c;
};

module.exports = TimeZone;