/*
* DateRngFmt.js - Date formatter definition
*
* Copyright © 2012-2015, 2018, 2020 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 dateformats sysres
var ilib = require("../index.js");
var JSUtils = require("./JSUtils.js");
var Locale = require("./Locale.js");
var CalendarFactory = require("./CalendarFactory.js");
var DateFmt = require("./DateFmt.js");
var IString = require("./IString.js");
var GregorianCal = require("./GregorianCal.js");
var DateFactory = require("./DateFactory.js");
/**
* @class
* Create a new date range formatter instance. The date range formatter is immutable once
* it is created, but can format as many different date ranges as needed with the same
* options. Create different date range formatter instances for different purposes
* and then keep them cached for use later if you have more than one range to
* format.<p>
*
* The options may contain any of the following properties:
*
* <ul>
* <li><i>locale</i> - locale to use when formatting the date/times in the range. If the
* locale is not specified, then the default locale of the app or web page will be used.
*
* <li><i>calendar</i> - the type of calendar to use for this format. The value should
* be a sting containing the name of the calendar. Currently, the supported
* types are "gregorian", "julian", "arabic", "hebrew", or "chinese". If the
* calendar is not specified, then the default calendar for the locale is used. When the
* calendar type is specified, then the format method must be called with an instance of
* the appropriate date type. (eg. Gregorian calendar means that the format method must
* be called with a GregDate instance.)
*
* <li><i>timezone</i> - time zone to use when formatting times. This may be a time zone
* instance or a time zone specifier string in RFC 822 format. If not specified, the
* default time zone for the locale is used.
*
* <li><i>length</i> - Specify the length of the format to use as a string. The length
* is the approximate size of the formatted string.
*
* <ul>
* <li><i>short</i> - use a short representation of the time. This is the most compact format possible for the locale.
* <li><i>medium</i> - use a medium length representation of the time. This is a slightly longer format.
* <li><i>long</i> - use a long representation of the time. This is a fully specified format, but some of the textual
* components may still be abbreviated. (eg. "Tue" instead of "Tuesday")
* <li><i>full</i> - use a full representation of the time. This is a fully specified format where all the textual
* components are spelled out completely.
* </ul>
* eg. The "short" format for an en_US range may be "MM/yy - MM/yy", whereas the long format might be
* "MMM, yyyy - MMM, yyyy". In the long format, the month name is textual instead of numeric
* and is longer, the year is 4 digits instead of 2, and the format contains slightly more
* spaces and formatting characters.<br><br>
* Note that the length parameter does not specify which components are to be formatted. The
* components that are formatted depend on the length of time in the range.
*
* <li><i>clock</i> - specify that formatted times should use a 12 or 24 hour clock if the
* format happens to include times. Valid values are "12" and "24".<br><br>
* In some locales, both clocks are used. For example, in en_US, the general populace uses
* a 12 hour clock with am/pm, but in the US military or in nautical or aeronautical or
* scientific writing, it is more common to use a 24 hour clock. This property allows you to
* construct a formatter that overrides the default for the locale.<br><br>
* If this property is not specified, the default is to use the most widely used convention
* for the locale.
* <li>onLoad - a callback function to call when the date range format object is fully
* loaded. When the onLoad option is given, the DateRngFmt object will attempt to
* load any missing locale data using the ilib loader callback.
* When the constructor is done (even if the data is already preassembled), the
* onLoad function is called with the current instance as a parameter, so this
* callback can be used with preassembled or dynamic loading or a mix of the two.
*
* <li>sync - 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>
* <p>
*
*
* @constructor
* @param {Object} options options governing the way this date range formatter instance works
*/
var DateRngFmt = function(options) {
var sync = true;
var loadParams = undefined;
this.locale = new Locale();
this.length = "s";
if (options) {
if (options.locale) {
this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
}
if (options.calendar) {
this.calName = options.calendar;
}
if (options.length) {
if (options.length === 'short' ||
options.length === 'medium' ||
options.length === 'long' ||
options.length === 'full') {
// only use the first char to save space in the json files
this.length = options.length.charAt(0);
}
}
if (options.timezone) {
this.timezone = options.timezone;
}
if (typeof(options.sync) !== 'undefined') {
sync = !!options.sync;
}
loadParams = options.loadParams;
}
var opts = {};
JSUtils.shallowCopy(options, opts);
opts.sync = sync;
opts.loadParams = loadParams;
/**
* @private
*/
opts.onLoad = ilib.bind(this, function (fmt) {
this.dateFmt = fmt;
if (fmt) {
this.locinfo = this.dateFmt.locinfo;
// get the default calendar name from the locale, and if the locale doesn't define
// one, use the hard-coded gregorian as the last resort
this.calName = this.calName || this.locinfo.getCalendar() || "gregorian";
CalendarFactory({
type: this.calName,
sync: sync,
loadParams: loadParams,
onLoad: ilib.bind(this, function(cal) {
this.cal = cal;
if (!this.cal) {
// always synchronous
this.cal = new GregorianCal();
}
this.timeTemplate = this.dateFmt._getFormat(this.dateFmt.formats.time[this.dateFmt.clock], this.dateFmt.timeComponents, this.length) || "hh:mm";
this.timeTemplateArr = this.dateFmt._tokenize(this.timeTemplate);
if (options && typeof(options.onLoad) === 'function') {
options.onLoad(this);
}
})
});
} else {
if (options && typeof(options.sync) === "boolean" && !options.sync && typeof(options.onLoad) === 'function') {
options.onLoad(undefined);
} else {
throw "No formats available for calendar " + this.calName + " in locale " + this.locale.getSpec();
}
}
});
// delegate a bunch of the formatting to this formatter
new DateFmt(opts);
};
DateRngFmt.prototype = {
/**
* Return the locale used with this formatter instance.
* @return {Locale} the Locale instance for this formatter
*/
getLocale: function() {
return this.locale;
},
/**
* Return the name of the calendar used to format date/times for this
* formatter instance.
* @return {string} the name of the calendar used by this formatter
*/
getCalendar: function () {
return this.dateFmt.getCalendar();
},
/**
* Return the length used to format date/times in this formatter. This is either the
* value of the length option to the constructor, or the default value.
*
* @return {string} the length of formats this formatter returns
*/
getLength: function () {
return DateFmt.lenmap[this.length] || "";
},
/**
* Return the time zone used to format date/times for this formatter
* instance.
* @return {TimeZone} a string naming the time zone
*/
getTimeZone: function () {
return this.dateFmt.getTimeZone();
},
/**
* Return the clock option set in the constructor. If the clock option was
* not given, the default from the locale is returned instead.
* @return {string} "12" or "24" depending on whether this formatter uses
* the 12-hour or 24-hour clock
*/
getClock: function () {
return this.dateFmt.getClock();
},
/**
* Format a date/time range according to the settings of the current
* formatter. The range is specified as being from the "start" date until
* the "end" date. <p>
*
* The template that the date/time range uses depends on the
* length of time between the dates, on the premise that a long date range
* which is too specific is not useful. For example, when giving
* the dates of the 100 Years War, in most situations it would be more
* appropriate to format the range as "1337 - 1453" than to format it as
* "10:37am November 9, 1337 - 4:37pm July 17, 1453", as the latter format
* is much too specific given the length of time that the range represents.
* If a very specific, but long, date range really is needed, the caller
* should format two specific dates separately and put them
* together as you might with other normal strings.<p>
*
* The format used for a date range contains the following date components,
* where the order of those components is rearranged and the component values
* are translated according to each locale:
*
* <ul>
* <li>within 3 days: the times of day, dates, months, and years
* <li>within 730 days (2 years): the dates, months, and years
* <li>within 3650 days (10 years): the months and years
* <li>longer than 10 years: the years only
* </ul>
*
* In general, if any of the date components share a value between the
* start and end date, that component is only given once. For example,
* if the range is from November 15, 2011 to November 26, 2011, the
* start and end dates both share the same month and year. The
* range would then be formatted as "November 15-26, 2011". <p>
*
* If you want to format a length of time instead of a particular range of
* time (for example, the length of an event rather than the specific start time
* and end time of that event), then use a duration formatter instance
* (DurationFmt) instead. The formatRange method will make sure that each component
* of the date/time is within the normal range for that component. For example,
* the minutes will always be between 0 and 59, no matter what is specified in
* the date to format, because that is the normal range for minutes. A duration
* format will allow the number of minutes to exceed 59. For example, if you
* were displaying the length of a movie that is 198 minutes long, the minutes
* component of a duration could be 198.<p>
*
* @param {IDate|Date|number|string} startDateLike the starting date/time of the range. The
* date may be given as an ilib IDate object, a javascript intrinsic Date object, a
* unix time, or a date string parsable by the javscript Date.
* @param {IDate|Date|number|string} endDateLike the ending date/time of the range. The
* date may be given as an ilib IDate object, a javascript intrinsic Date object, a
* unix time, or a date string parsable by the javscript Date.
* @throws "Wrong calendar type" when the start or end dates are not the same
* calendar type as the formatter itself
* @return {string} a date range formatted for the locale
*/
format: function (startDateLike, endDateLike) {
var startRd, endRd, fmt = "", yearTemplate, monthTemplate, dayTemplate, formats;
var thisZoneName = this.dateFmt.tz && this.dateFmt.tz.getId() || "local";
var start = DateFactory._dateToIlib(startDateLike, thisZoneName, this.locale);
var end = DateFactory._dateToIlib(endDateLike, thisZoneName, this.locale);
var tmp;
if (typeof(start) !== 'object' || !start.getCalendar || start.getCalendar() !== this.calName || (this.timezone && start.timezone && start.timezone !== this.timezone)) {
start = DateFactory({
type: this.calName,
timezone: thisZoneName,
julianday: start.getJulianDay()
});
}
if (typeof(end) !== 'object' || !end.getCalendar || end.getCalendar() !== this.calName || (this.timezone && end.timezone && end.timezone !== this.timezone)) {
end = DateFactory({
type: this.calName,
timezone: thisZoneName,
julianday: end.getJulianDay()
});
}
startRd = start.getRataDie();
endRd = end.getRataDie();
formats = this.dateFmt.formats.date;
// these are not stand-alone templates. We add stand-alone below if necessary for the locale and the format.
yearTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "y", this.length) || "yyyy");
monthTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "m", this.length) || "MM");
dayTemplate = this.dateFmt._tokenize(this.dateFmt._getFormatInternal(formats, "d", this.length) || "dd");
//
// legend:
// c00 - difference is less than 3 days. Year, month, and date are same, but time is different
// c01 - difference is less than 3 days. Year and month are same but date and time are different
// c02 - difference is less than 3 days. Year is same but month, date, and time are different. (ie. it straddles a month boundary)
// c03 - difference is less than 3 days. Year, month, date, and time are all different. (ie. it straddles a year boundary)
// c10 - difference is less than 2 years. Year and month are the same, but date is different.
// c11 - difference is less than 2 years. Year is the same, but month, date, and time are different.
// c12 - difference is less than 2 years. All fields are different. (ie. straddles a year boundary)
// c20 - difference is less than 10 years. All fields are different.
// c30 - difference is more than 10 years. All fields are different.
//
if (endRd - startRd < 3) {
if (start.year === end.year) {
if (start.month === end.month) {
if (start.day === end.day) {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c00", this.length));
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c01", this.length));
}
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c02", this.length));
}
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c03", this.length));
}
} else if (endRd - startRd < 730) {
if (start.year === end.year) {
if (start.month === end.month) {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c10", this.length));
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c11", this.length));
}
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c12", this.length));
}
} else if (endRd - startRd < 3650) {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c20", this.length));
// need to check for month/year stand-alone formats here
tmp = this.dateFmt._getFormatInternal(formats, "mys", this.length);
if (tmp) {
if (tmp.indexOf('r') > -1) {
var tmp2 = this.dateFmt._getFormatInternal(formats, "r", this.length);
if (tmp2) {
yearTemplate = this.dateFmt._tokenize(tmp2);
}
}
if (tmp.indexOf('L') > -1) {
var tmp3 = this.dateFmt._getFormatInternal(formats, "l", this.length);
if (tmp3) {
monthTemplate = this.dateFmt._tokenize(tmp3);
}
}
}
} else {
fmt = new IString(this.dateFmt._getFormat(this.dateFmt.formats.range, "c30", this.length));
// need to check for stand-alone year formats here
tmp = this.dateFmt._getFormatInternal(formats, "r", this.length);
if (tmp) {
yearTemplate = this.dateFmt._tokenize(tmp);
}
}
/*
console.log("fmt is " + fmt.toString());
console.log("year template is " + yearTemplate);
console.log("month template is " + monthTemplate);
console.log("day template is " + dayTemplate);
*/
return fmt.format({
sy: this.dateFmt._formatTemplate(start, yearTemplate),
sm: this.dateFmt._formatTemplate(start, monthTemplate),
sd: this.dateFmt._formatTemplate(start, dayTemplate),
st: this.dateFmt._formatTemplate(start, this.timeTemplateArr),
ey: this.dateFmt._formatTemplate(end, yearTemplate),
em: this.dateFmt._formatTemplate(end, monthTemplate),
ed: this.dateFmt._formatTemplate(end, dayTemplate),
et: this.dateFmt._formatTemplate(end, this.timeTemplateArr)
});
}
};
module.exports = DateRngFmt;
Source