hijrah-date
Version:
A date in the Hijrah calendar system
568 lines (500 loc) • 20.6 kB
JavaScript
/*
Original work Copyright (c) 2010-2016 Google, Inc. http://angularjs.org
Modified work Copyright (c) 2016 Mouaffak A. Sarhan
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 HijrahDateFormatter = (function(){
;
var MAX_DIGITS = 22;
var DECIMAL_SEP = '.';
var ZERO_CHAR = '0';
var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
// IE/Edge do not "understand" colon (`:`) in timezone
timezone = timezone.replace(ALL_COLONS, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
function addDateMinutes(date, minutes) {
date = new Date(date.getTime());
date.setMinutes(date.getMinutes() + minutes);
return date;
}
function convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
var dateTimezoneOffset = date.getTimezoneOffset();
var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
}
/**
* Parse a number (as a string) into three components that can be used
* for formatting the number.
*
* (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
*
* @param {string} numStr The number to parse
* @return {object} An object describing this number, containing the following keys:
* - d : an array of digits containing leading zeros as necessary
* - i : the number of the digits in `d` that are to the left of the decimal point
* - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
*
*/
function parse(numStr) {
var exponent = 0, digits, numberOfIntegerDigits;
var i, j, zeros;
// Decimal point?
if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
numStr = numStr.replace(DECIMAL_SEP, '');
}
// Exponential form?
if ((i = numStr.search(/e/i)) > 0) {
// Work out the exponent.
if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
numberOfIntegerDigits += +numStr.slice(i + 1);
numStr = numStr.substring(0, i);
} else if (numberOfIntegerDigits < 0) {
// There was no decimal point or exponent so it is an integer.
numberOfIntegerDigits = numStr.length;
}
// Count the number of leading zeros.
for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
if (i === (zeros = numStr.length)) {
// The digits are all zero.
digits = [0];
numberOfIntegerDigits = 1;
} else {
// Count the number of trailing zeros
zeros--;
while (numStr.charAt(zeros) === ZERO_CHAR) zeros--;
// Trailing zeros are insignificant so ignore them
numberOfIntegerDigits -= i;
digits = [];
// Convert string to array of digits without leading/trailing zeros.
for (j = 0; i <= zeros; i++, j++) {
digits[j] = +numStr.charAt(i);
}
}
// If the number overflows the maximum allowed digits then use an exponent.
if (numberOfIntegerDigits > MAX_DIGITS) {
digits = digits.splice(0, MAX_DIGITS - 1);
exponent = numberOfIntegerDigits - 1;
numberOfIntegerDigits = 1;
}
return { d: digits, e: exponent, i: numberOfIntegerDigits };
}
/**
* Round the parsed number to the specified number of decimal places
* This function changed the parsedNumber in-place
*/
function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
var digits = parsedNumber.d;
var fractionLen = digits.length - parsedNumber.i;
// determine fractionSize if it is not specified; `+fractionSize` converts it to a number
fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
// The index of the digit to where rounding is to occur
var roundAt = fractionSize + parsedNumber.i;
var digit = digits[roundAt];
if (roundAt > 0) {
// Drop fractional digits beyond `roundAt`
digits.splice(Math.max(parsedNumber.i, roundAt));
// Set non-fractional digits beyond `roundAt` to 0
for (var j = roundAt; j < digits.length; j++) {
digits[j] = 0;
}
} else {
// We rounded to zero so reset the parsedNumber
fractionLen = Math.max(0, fractionLen);
parsedNumber.i = 1;
digits.length = Math.max(1, roundAt = fractionSize + 1);
digits[0] = 0;
for (var i = 1; i < roundAt; i++) digits[i] = 0;
}
if (digit >= 5) {
if (roundAt - 1 < 0) {
for (var k = 0; k > roundAt; k--) {
digits.unshift(0);
parsedNumber.i++;
}
digits.unshift(1);
parsedNumber.i++;
} else {
digits[roundAt - 1]++;
}
}
// Pad out with zeros to get the required fraction length
for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
// Do any carrying, e.g. a digit was rounded up to 10
var carry = digits.reduceRight(function(carry, d, i, digits) {
d = d + carry;
digits[i] = d % 10;
return Math.floor(d / 10);
}, 0);
if (carry) {
digits.unshift(carry);
parsedNumber.i++;
}
}
/**
* Format a number into a string
* @param {number} number The number to format
* @param {{
* minFrac, // the minimum number of digits required in the fraction part of the number
* maxFrac, // the maximum number of digits required in the fraction part of the number
* gSize, // number of digits in each group of separated digits
* lgSize, // number of digits in the last group of digits before the decimal separator
* negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
* posPre, // the string to go in front of a positive number
* negSuf, // the string to go after a negative number (e.g. `)`)
* posSuf // the string to go after a positive number
* }} pattern
* @param {string} groupSep The string to separate groups of number (e.g. `,`)
* @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
* @param {[type]} fractionSize The size of the fractional part of the number
* @return {string} The number formatted as a string
*/
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
var isInfinity = !isFinite(number);
var isZero = false;
var numStr = Math.abs(number) + '',
formattedText = '',
parsedNumber;
if (isInfinity) {
formattedText = '\u221e';
} else {
parsedNumber = parse(numStr);
roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
var digits = parsedNumber.d;
var integerLen = parsedNumber.i;
var exponent = parsedNumber.e;
var decimals = [];
isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
// pad zeros for small numbers
while (integerLen < 0) {
digits.unshift(0);
integerLen++;
}
// extract decimals digits
if (integerLen > 0) {
decimals = digits.splice(integerLen, digits.length);
} else {
decimals = digits;
digits = [0];
}
// format the integer digits with grouping separators
var groups = [];
if (digits.length >= pattern.lgSize) {
groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
}
while (digits.length > pattern.gSize) {
groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
}
if (digits.length) {
groups.unshift(digits.join(''));
}
formattedText = groups.join(groupSep);
// append the decimal digits
if (decimals.length) {
formattedText += decimalSep + decimals.join('');
}
if (exponent) {
formattedText += 'e+' + exponent;
}
}
if (number < 0 && !isZero) {
return pattern.negPre + formattedText + pattern.negSuf;
} else {
return pattern.posPre + formattedText + pattern.posSuf;
}
}
function padNumber(num, digits, trim, negWrap) {
var neg = '';
if (num < 0 || (negWrap && num <= 0)) {
if (negWrap) {
num = -num + 1;
} else {
num = -num;
neg = '-';
}
}
num = '' + num;
while (num.length < digits) num = ZERO_CHAR + num;
if (trim) {
num = num.substr(num.length - digits);
}
return neg + num;
}
function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
if (offset > 0 || value > -offset) {
value += offset;
}
if (value === 0 && offset === -12) value = 12;
return padNumber(value, size, trim, negWrap);
};
}
function dateStrGetter(name, shortForm, standAlone) {
return function(date, formats) {
var value = date['get' + name]();
var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
var get = uppercase(propPrefix + name);
return formats[get][value];
};
}
function timeZoneGetter(date, formats, offset) {
var zone = -1 * offset;
var paddedZone = (zone >= 0) ? '+' : '';
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
padNumber(Math.abs(zone % 60), 2);
return paddedZone;
}
function getFirstThursdayOfYear(year) {
// 0 = index of January
var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay();
// 4 = index of Thursday (+1 to account for 1st = 5)
// 11 = index of *next* Thursday (+1 account for 1st = 12)
return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst);
}
function getThursdayThisWeek(datetime) {
return new Date(datetime.getFullYear(), datetime.getMonth(),
// 4 = index of Thursday
datetime.getDate() + (4 - datetime.getDay()));
}
function weekGetter(size) {
return function(hijrahDate) {
var result = toInt(((hijrahDate.getDayOfYear() - 1) / 7)) + 1;
/*
var firstThurs = getFirstThursdayOfYear(date.getFullYear()),
thisThurs = getThursdayThisWeek(date);
var diff = +thisThurs - +firstThurs,
result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
*/
return padNumber(result, size);
};
}
function ampmGetter(date, formats) {
return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
}
function eraGetter(date, formats) {
return formats.ERAS[0];
}
function longEraGetter(date, formats) {
return formats.ERANAMES[0];
}
var DATE_FORMATS = {
yyyy: dateGetter('FullYear', 4, 0, false, true),
yy: dateGetter('FullYear', 2, 0, true, true),
y: dateGetter('FullYear', 1, 0, false, true),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
LLLL: dateStrGetter('Month', false, true),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
H: dateGetter('Hours', 1),
hh: dateGetter('Hours', 2, -12),
h: dateGetter('Hours', 1, -12),
mm: dateGetter('Minutes', 2),
m: dateGetter('Minutes', 1),
ss: dateGetter('Seconds', 2),
s: dateGetter('Seconds', 1),
// while ISO 8601 requires fractions to be prefixed with `.` or `,`
// we can be just safely rely on using `sss` since we currently don't support single or two digit fractions
sss: dateGetter('Milliseconds', 3),
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true),
a: ampmGetter,
Z: timeZoneGetter,
ww: weekGetter(2),
w: weekGetter(1),
G: eraGetter,
GG: eraGetter,
GGG: eraGetter,
GGGG: longEraGetter
};
var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;
/**
* @ngdoc filter
* @name date
* @kind function
*
* @description
* Formats `date` to a string based on the requested `format`.
*
* `format` string can be composed of the following elements:
*
* * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010)
* * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10)
* * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199)
* * `'MMMM'`: Month in year (January-December)
* * `'MMM'`: Month in year (Jan-Dec)
* * `'MM'`: Month in year, padded (01-12)
* * `'M'`: Month in year (1-12)
* * `'LLLL'`: Stand-alone month in year (January-December)
* * `'dd'`: Day in month, padded (01-31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
* * `'EEE'`: Day in Week, (Sun-Sat)
* * `'HH'`: Hour in day, padded (00-23)
* * `'H'`: Hour in day (0-23)
* * `'hh'`: Hour in AM/PM, padded (01-12)
* * `'h'`: Hour in AM/PM, (1-12)
* * `'mm'`: Minute in hour, padded (00-59)
* * `'m'`: Minute in hour (0-59)
* * `'ss'`: Second in minute, padded (00-59)
* * `'s'`: Second in minute (0-59)
* * `'sss'`: Millisecond in second, padded (000-999)
* * `'a'`: AM/PM marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
* * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year
* * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year
* * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD')
* * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini')
*
* `format` string can also be one of the following predefined
* {@link guide/i18n localizable formats}:
*
* * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
* (e.g. Sep 3, 2010 12:05:08 PM)
* * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM)
* * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale
* (e.g. Friday, September 3, 2010)
* * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010)
* * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
* * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
* * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM)
* * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM)
*
* `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g.
* `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
* (e.g. `"h 'o''clock'"`).
*
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
* specified in the string input, the time is considered to be in the local timezone.
* @param {string=} format Formatting rules (see Description). If not specified,
* `mediumDate` is used.
* @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
* continental US time zone abbreviations, but for general use, use a time zone offset, for
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
* If not specified, the timezone of the browser will be used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
<example name="filter-date">
<file name="index.html">
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>:
<span>{{1288323623006 | date:'medium'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>:
<span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
<span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br>
<span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>:
<span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br>
</file>
<file name="protractor.js" type="protractor">
it('should format date', function() {
expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/);
});
</file>
</example>
*/
//dateFilter.$inject = ['$locale'];
function dateFilter($locale) {
var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
// 1 2 3 4 5 6 7 8 9 10 11
function jsonStringToDate(string) {
var match;
if ((match = string.match(R_ISO8601_STR))) {
var date = new HijrahDate(new Date(0)),
tzHour = 0,
tzMin = 0,
dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear,
timeSetter = match[8] ? date.setUTCHours : date.setHours;
if (match[9]) {
tzHour = toInt(match[9] + match[10]);
tzMin = toInt(match[9] + match[11]);
}
dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
var h = toInt(match[4] || 0) - tzHour;
var m = toInt(match[5] || 0) - tzMin;
var s = toInt(match[6] || 0);
var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
}
return string;
}
return function(hijrahDate, format, timezone) {
var text = '',
parts = [],
fn, match;
format = format || 'mediumDate';
format = $locale.DATETIME_FORMATS[format] || format;
if (isString(hijrahDate)) {
hijrahDate = NUMBER_STRING.test(hijrahDate) ? toInt(hijrahDate) : jsonStringToDate(hijrahDate);
}
if (isNumber(hijrahDate)) {
hijrahDate = new HijrahDate(hijrahDate);
}
if (!HijrahDate.isHijrahDate(hijrahDate) || !isFinite(hijrahDate.getTime())) {
return hijrahDate;
}
while (format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
parts = concat(parts, match, 1);
format = parts.pop();
} else {
parts.push(format);
format = null;
}
}
var date = hijrahDate.toGregorian();
var dateTimezoneOffset = date.getTimezoneOffset();
if (timezone) {
dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
date = convertTimezoneToLocal(date, timezone, true);
hijrahDate = new HijrahDate(date);
}
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
text += fn ? fn(hijrahDate, $locale.DATETIME_FORMATS, dateTimezoneOffset)
: value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
});
return text;
};
}
return {
formatter: function(localeId){
return dateFilter(LocaleProvider.getLocale(localeId));
}
};
})();