@enact/i18n
Version:
Internationalization support for Enact using iLib
520 lines (481 loc) • 17.9 kB
JavaScript
"use strict";
/* global XMLHttpRequest */
/*
* zoneinfo.js - represent a binary zone info file
*
* Copyright © 2014 LG Electronics, Inc.
*
* 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.
*
* The portion of this code that parses the zone info file format is derived
* from the code in the node-zoneinfo project by Gregory McWhirter licensed
* under the MIT license:
*
* Copyright (c) 2013 Gregory McWhirter
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject
* to the following conditions:
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
var PackedBuffer = require('./packedbuffer.js');
var _platform = typeof window !== 'undefined' && (window.webOSSystem || window.PalmSystem) ? 'webos' : 'browser';
/**
* Represents a binary zone info file of the sort that the Unix Zone Info Compiler
* produces.
* @constructor
* @param {string} path path to the file to be loaded
* @param {number} year year of the zone info rules needed
*/
var ZoneInfoFile = function ZoneInfoFile(path) {
var that = this;
switch (_platform) {
/*
Uncomment and use this when enyo works for binary load.
case 'enyo':
let ajax = new enyo.Ajax({
xhrFields: {
responseType:'arraybuffer'
},
cacheBust: false,
sync: true,
handleAs: 'binary',
url: 'file://' + path
});
ajax.response(this, function (s, r) {
let byteArray = new Uint8Array(r);
// console.log('ZoneInfoFile bytes received: ' + byteArray.length);
that._parseInfo(byteArray);
});
//ajax.error(this, function (s, r) {
// console.log('ZoneInfoFile: failed to load files ' + JSON.stringify(s) + ' ' + r);
//});
ajax.go();
break;
*/
default:
{
// use normal web techniques for sync binary data fetching
// see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data
var req = new XMLHttpRequest();
req.open('GET', 'file:' + path, false);
req.overrideMimeType('text/plain; charset=x-user-defined');
req.onload = function () {
var byteArray = new Uint8Array(req.response.length);
for (var i = 0; i < req.response.length; i++) {
byteArray[i] = req.response.charCodeAt(i) & 0xff;
}
// console.log('ZoneInfoFile bytes received: ' + byteArray.length);
that._parseInfo(byteArray);
};
req.onerror = function () {
throw new Error('Cannot load file ' + path);
};
req.send();
break;
}
}
};
/**
* @private
* Parse the binary buffer to find the zone info
* @param {Buffer} buffer The buffer to process
* @returns {void}
*/
ZoneInfoFile.prototype._parseInfo = function (buffer) {
var packed = new PackedBuffer(buffer);
// The time zone information files used by tzset(3)
// begin with the magic characters 'TZif' to identify
// them as time zone information files, followed by
// sixteen bytes reserved for future use, followed by
// six four-byte values of type long, written in a
// ''standard'' byte order (the high-order byte
// of the value is written first).
if (packed.getString(4) !== 'TZif') {
throw new Error('file format not recognized');
} else {
// ignore 16 bytes
packed.skip(16);
/* eslint-disable camelcase */
// The number of UTC/local indicators stored in the file.
var tzh_ttisgmtcnt = packed.getLong();
// The number of standard/wall indicators stored in the file.
var tzh_ttisstdcnt = packed.getLong();
// The number of leap seconds for which data is stored in the file.
var tzh_leapcnt = packed.getLong();
// The number of 'transition times' for which data is stored in the file.
var tzh_timecnt = packed.getLong();
// The number of 'local time types' for which data is stored in the file (must not be zero).
var tzh_typecnt = packed.getLong();
// The number of characters of 'time zone abbreviation strings' stored in the file.
var tzh_charcnt = packed.getLong();
this.transitionTimes = tzh_timecnt ? packed.getLongs(tzh_timecnt) : [];
this.transitionTimes = this.transitionTimes.map(function (item) {
return item * 1000;
});
// these are indexes into the zonesInfo that correspond to each transition time
this.ruleIndex = tzh_timecnt ? packed.getUnsignedBytes(tzh_timecnt) : [];
this.zoneInfo = [];
for (var i = 0; i < tzh_typecnt; i++) {
this.zoneInfo.push({
offset: Math.floor(packed.getLong() / 60),
// offset in seconds, so convert to minutes
isdst: !!packed.getByte(),
abbreviationIndex: packed.getByte()
});
}
var allAbbreviations = packed.getString(tzh_charcnt);
for (var _i = 0; _i < tzh_typecnt; _i++) {
var abbreviation = allAbbreviations.substring(this.zoneInfo[_i].abbreviationIndex);
this.zoneInfo[_i].abbreviation = abbreviation.substring(0, abbreviation.indexOf('\x00'));
}
// ignore the leap seconds
if (tzh_leapcnt) {
packed.skip(tzh_leapcnt * 2);
}
// skip the standard/wall time indicators
if (tzh_ttisstdcnt) {
packed.skip(tzh_ttisstdcnt);
}
// ignore the UTC/local time indicators -- everything should be UTC
if (tzh_ttisgmtcnt) {
packed.skip(tzh_ttisgmtcnt);
}
// finished reading
// Replace ttinfo indexes for ttinfo objects.
var that = this;
this.ruleIndex = this.ruleIndex.map(function (item) {
return {
offset: that.zoneInfo[item].offset,
isdst: that.zoneInfo[item].isdst,
abbreviation: that.zoneInfo[item].abbreviation
};
});
// calculate the dst savings for each daylight time
for (var _i2 = 0; _i2 < tzh_timecnt; _i2++) {
if (_i2 > 0 && this.ruleIndex[_i2].isdst) {
this.ruleIndex[_i2].savings = this.ruleIndex[_i2].offset - this.ruleIndex[_i2 - 1].offset;
}
}
// Set standard, dst, and before ttinfos. before will be
// used when a given time is before any transitions,
// and will be set to the first non-dst ttinfo, or to
// the first dst, if all of them are dst.
if (!this.transitionTimes.length) {
this.standardTime = this.zoneInfo[0];
} else {
for (var j = tzh_timecnt - 1; j > -1; j--) {
var tti = this.ruleIndex[j];
if (!this.standardTime && !tti.isdst) {
this.standardTime = tti;
} else if (!this.daylightTime && tti.isdst) {
this.daylightTime = tti;
}
if (this.daylightTime && this.standardTime) {
break;
}
}
if (this.daylightTime && !this.standardTime) {
this.standardTime = this.daylightTime;
}
for (var k = this.zoneInfo.length - 1; k > 0; k--) {
if (!this.zoneInfo[k].isdst) {
this.defaultTime = this.zoneInfo[k];
break;
}
}
}
if (!this.defaultTime) {
this.defaultTime = this.zoneInfo[this.zoneInfo.length - 1];
}
}
};
/**
* Binary search a sorted array of numbers for a particular target value.
* If the exact value is not found, it returns the index of the largest
* entry that is smaller than the given target value.<p>
*
* @param {number} target element being sought
* @param {Array} arr the array being searched
* @returns {number} the index of the array into which the value would fit if
* inserted, or -1 if given array is not an array or the target is not
* a number
*/
ZoneInfoFile.prototype.bsearch = function (target, arr) {
if (typeof arr === 'undefined' || !arr || typeof target === 'undefined' || target < arr[0]) {
return -1;
}
// greater than the end of the array
if (target > arr[arr.length - 1]) {
return arr.length - 1;
}
var high = arr.length - 1,
low = 0,
mid = 0,
value;
while (low <= high) {
mid = Math.floor((high + low) / 2);
value = arr[mid] - target;
if (value > 0) {
high = mid - 1;
} else if (value < 0) {
low = mid + 1;
} else {
return mid;
}
}
return high;
};
/**
* Return whether or not this zone uses DST in the given year.
*
* @param {Date} date the Gregorian date to test
* @returns {boolean} true if the zone uses DST in the given year
*/
ZoneInfoFile.prototype.usesDST = function (date) {
var thisYear = date.getTime();
var nextYear = thisYear + 31536000000; // this is the number of ms in 1 Gregorian year
// search for the zone that was effective Jan 1 of this year
// to Jan 1 of next year, and if any of the infos is DST, then
// this zone supports DST in the given year.
var index = this.bsearch(thisYear, this.transitionTimes);
if (index !== -1) {
while (index < this.transitionTimes.length && this.transitionTimes[index] < nextYear) {
if (this.ruleIndex[index++].isdst) {
return true;
}
}
}
return false;
};
/**
* Return the raw offset from UTC that this zone uses at the given time.
*
* @param {Date} date the Gregorian date to test
* @returns {number} offset from UTC in number of minutes. Negative
* numbers are west of Greenwich, positive are east of Greenwich
*/
ZoneInfoFile.prototype.getRawOffset = function (date) {
var thisYear = date.getTime();
var nextYear = thisYear + 31536000000; // this is the number of ms in 1 Gregorian year
var index = this.bsearch(thisYear, this.transitionTimes);
var offset = this.defaultTime.offset;
if (index > -1) {
while (index < this.transitionTimes.length && this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && !this.ruleIndex[index].isdst) {
offset = this.ruleIndex[index].offset;
}
}
return offset;
};
/**
* If this zone uses DST in the given year, return the DST savings
* in use. If the zone does not use DST in the given year, this
* method will return 0.
*
* @param {Date} date the Gregorian date to test
* @returns {number} number of minutes in DST savings if the zone
* uses DST in the given year, or zero otherwise
*/
ZoneInfoFile.prototype.getDSTSavings = function (date) {
var thisYear = date.getTime();
var nextYear = thisYear + 31536000000; // this is the number of ms in 1 Gregorian year
// search for all transitions between now and one year
// from now, and calculate the difference in DST (if any)
var index = this.bsearch(thisYear, this.transitionTimes);
var savings = 0;
if (index > -1) {
while (index < this.transitionTimes.length && !this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && this.ruleIndex[index].isdst) {
savings = this.ruleIndex[index].savings;
}
}
return savings;
};
/**
* Return the start date/time of DST if this zone uses
* DST in the given year.
*
* @param {Date} date the Gregorian date to test
* @returns {number} unixtime representation of the start
* of DST in the given year, or -1 if the zone does not
* use DST in the given year
*/
ZoneInfoFile.prototype.getDSTStartDate = function (date) {
var year = date.getFullYear();
var thisYear = new Date(year, 0, 1).getTime();
var nextYear = new Date(year + 1, 0, 1).getTime();
// search for all transitions between Jan 1 of this year
// to Jan 1 of next year, and calculate the difference
// in DST (if any)
var index = this.bsearch(thisYear, this.transitionTimes);
var startDate = -1;
if (index > -1) {
if (this.transitionTimes[index] < thisYear) {
index++; // start in this year instead of the previous year
}
while (index < this.transitionTimes.length && !this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && this.ruleIndex[index].isdst) {
startDate = this.transitionTimes[index];
}
}
return startDate;
};
/**
* Return the end date/time of DST if this zone uses
* DST in the given year.
*
* @param {Date} date the Gregorian date to test
* @returns {number} unixtime representation of the end
* of DST in the given year, or -1 if the zone does not
* use DST in the given year
*/
ZoneInfoFile.prototype.getDSTEndDate = function (date) {
var year = date.getFullYear();
var thisYear = new Date(year, 0, 1).getTime();
var nextYear = new Date(year + 1, 0, 1).getTime();
// search for all transitions between Jan 1 of this year
// to Jan 1 of next year, and calculate the difference
// in DST (if any)
var index = this.bsearch(thisYear, this.transitionTimes);
var endDate = -1;
if (index > -1) {
if (this.transitionTimes[index] < thisYear) {
index++; // start in this year instead of the previous year
}
while (index < this.transitionTimes.length && this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && !this.ruleIndex[index].isdst) {
endDate = this.transitionTimes[index];
}
}
return endDate;
};
/**
* Return the abbreviation used by this zone in standard
* time.
*
* @param {Date} date the Gregorian date to test
* @returns {string} a string representing the abbreviation
* used in this time zone during standard time
*/
ZoneInfoFile.prototype.getAbbreviation = function (date) {
var thisYear = date.getTime();
var nextYear = thisYear + 31536000000; // this is the number of ms in 1 Gregorian year
// search for all transitions between now and one year from now, and calculate the difference
// in DST (if any)
var abbr;
if (this.transitionTimes.length > 0) {
var index = this.bsearch(thisYear, this.transitionTimes);
abbr = this.ruleIndex[index].abbreviation;
if (index > -1) {
while (index < this.transitionTimes.length && this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && !this.ruleIndex[index].isdst) {
abbr = this.ruleIndex[index].abbreviation;
}
}
} else {
abbr = this.standardTime.abbreviation;
}
return abbr;
};
/**
* Return the abbreviation used by this zone in daylight
* time. If the zone does not use DST in the given year,
* this returns the same thing as getAbbreviation().
*
* @param {Date} date the Gregorian date to test
* @returns {string} a string representing the abbreviation
* used in this time zone during daylight time
*/
ZoneInfoFile.prototype.getDSTAbbreviation = function (date) {
var thisYear = date.getTime();
var nextYear = thisYear + 31536000000; // this is the number of ms in 1 Gregorian year
// search for all transitions between now and one year from now, and calculate the difference
// in DST (if any)
var abbr;
if (this.transitionTimes.length > 0) {
var index = this.bsearch(thisYear, this.transitionTimes);
abbr = this.ruleIndex[index].abbreviation;
if (index > -1) {
while (index < this.transitionTimes.length && !this.ruleIndex[index].isdst && this.transitionTimes[index + 1] < nextYear) {
index++;
}
if (index < this.transitionTimes.length && this.ruleIndex[index].isdst) {
abbr = this.ruleIndex[index].abbreviation;
}
}
} else {
abbr = this.standardTime.abbreviation;
}
return abbr;
};
/**
* Return the zone information for the given date in ilib
* format.
*
* @param {Date} date the Gregorian date to test
* @returns {Object} an object containing the zone information
* for the given date in the format that ilib can use directly
*/
ZoneInfoFile.prototype.getIlibZoneInfo = function (date) {
function minutesToStr(min) {
var hours = Math.floor(min / 60);
var minutes = min - hours * 60;
return hours + ':' + minutes;
}
function unixtimeToJD(millis) {
return 2440587.5 + millis / 86400000;
}
var res = {
'o': minutesToStr(this.getRawOffset(date))
};
if (this.usesDST(date)) {
res.f = '{c}';
res.e = {
'c': this.getAbbreviation(date),
'j': unixtimeToJD(this.getDSTEndDate(date))
};
res.s = {
'c': this.getDSTAbbreviation(date),
'j': unixtimeToJD(this.getDSTStartDate(date)),
'v': minutesToStr(this.getDSTSavings(date))
};
} else {
res.f = this.getAbbreviation(date);
}
return res;
};
module.exports = ZoneInfoFile;