@eclipse-scout/core
Version:
Eclipse Scout runtime
645 lines (595 loc) • 22 kB
text/typescript
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {DateFormat, DateRange, Locale, objects, scout, strings, texts} from '../index';
export interface JsonDateRange {
from: string;
to: string;
}
export const dates = {
shift(date: Date, years: number, months?: number, days?: number): Date {
let newDate = new Date(date.getTime());
if (years) {
newDate.setFullYear(date.getFullYear() + years);
if (dates.compareMonths(newDate, date) !== years * 12) {
// Set to last day of the previous month
// The reason: 2016-02-29 + 1 year -> 2017-03-01 instead of 2017-02-28
newDate.setDate(0);
}
}
if (months) {
newDate.setMonth(date.getMonth() + months);
if (dates.compareMonths(newDate, date) !== months + years * 12) {
// Set to last day of the previous month
// The reason: 2010-10-31 + 1 month -> 2010-12-01 instead of 2010-11-30
newDate.setDate(0);
}
}
if (days) {
newDate.setDate(date.getDate() + days);
}
return newDate;
},
shiftTime(date: Date, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): Date {
let newDate = new Date(date.getTime());
if (hours) {
newDate.setHours(date.getHours() + hours);
}
if (minutes) {
newDate.setMinutes(date.getMinutes() + minutes);
}
if (seconds) {
newDate.setSeconds(date.getSeconds() + seconds);
}
if (milliseconds) {
newDate.setMilliseconds(date.getMilliseconds() + milliseconds);
}
return newDate;
},
shiftToNextDayOfType(date: Date, day: number): Date {
let diff = day - date.getDay();
if (diff <= 0) {
diff += 7;
}
return dates.shift(date, 0, 0, diff);
},
/**
* Finds the next date (based on the given date) that matches the given day in week and date.
*
* @param date Start date
* @param dayInWeek 0-6
* @param dayInMonth 1-31
*/
shiftToNextDayAndDate(date: Date, dayInWeek: number, dayInMonth: number): Date {
let tmpDate = new Date(date.getTime());
if (dayInMonth < tmpDate.getDate()) {
tmpDate.setMonth(tmpDate.getMonth() + 1);
}
tmpDate.setDate(dayInMonth);
while (tmpDate.getDay() !== dayInWeek || tmpDate.getDate() !== dayInMonth) {
tmpDate = dates.shift(tmpDate, 0, 1, 0);
tmpDate.setDate(dayInMonth);
}
return tmpDate;
},
shiftToPreviousDayOfType(date: Date, day: number): Date {
let diff = day - date.getDay();
if (diff >= 0) {
diff -= 7;
}
return dates.shift(date, 0, 0, diff);
},
shiftToNextOrPrevDayOfType(date: Date, day: number, direction: number): Date {
if (direction > 0) {
return dates.shiftToNextDayOfType(date, day);
}
return dates.shiftToPreviousDayOfType(date, day);
},
shiftToNextOrPrevMonday(date: Date, direction: number): Date {
return dates.shiftToNextOrPrevDayOfType(date, 1, direction);
},
/**
* Ensures that the given date is really a date.
* <p>
* If it already is a date, the date will be returned.
* Otherwise parseJsonDate is used to create a Date.
*
* @param date may be of type date or string.
*/
ensure(date: Date | string): Date {
if (objects.isNullOrUndefined(date)) {
return date as Date;
}
if (date instanceof Date) {
return date;
}
return dates.parseJsonDate(date);
},
ensureMonday(date: Date, direction: number): Date {
if (date.getDay() === 1) {
return date;
}
return dates.shiftToNextOrPrevMonday(date, direction);
},
isSameTime(date: Date, date2: Date): boolean {
if (!date || !date2) {
return false;
}
return date.getHours() === date2.getHours() &&
date.getMinutes() === date2.getMinutes() &&
date.getSeconds() === date2.getSeconds();
},
isSameDay(date: Date, date2: Date): boolean {
if (!date || !date2) {
return false;
}
return date.getFullYear() === date2.getFullYear() &&
date.getMonth() === date2.getMonth() &&
date.getDate() === date2.getDate();
},
isSameMonth(date: Date, date2: Date): boolean {
if (!date || !date2) {
return false;
}
return dates.compareMonths(date, date2) === 0;
},
/**
* Returns the difference of the two dates in number of months.
*/
compareMonths(date1: Date, date2: Date): number {
let d1Month = date1.getMonth(),
d2Month = date2.getMonth(),
d1Year = date1.getFullYear(),
d2Year = date2.getFullYear(),
monthDiff = d1Month - d2Month;
if (d1Year === d2Year) {
return monthDiff;
}
return (d1Year - d2Year) * 12 + monthDiff;
},
/**
* Returns the difference of the two dates in number of days.
*/
compareDays(date1: Date, date2: Date): number {
return (dates.trunc(date1).getTime() - dates.trunc(date2).getTime() - (date1.getTimezoneOffset() - date2.getTimezoneOffset()) * 60000) / (3600000 * 24);
},
orderWeekdays(weekdays: string[], firstDayOfWeekArg: number): string[] {
let weekdaysOrdered: string[] = [];
for (let i = 0; i < 7; i++) {
weekdaysOrdered[i] = weekdays[(i + firstDayOfWeekArg) % 7];
}
return weekdaysOrdered;
},
/**
* Returns the week number according to ISO 8601 definition:
* - All years have 52 or 53 weeks.
* - The first week is the week with January 4th in it.
* - The first day of a week is Monday, the last day is Sunday
*
* This is the default behavior. By setting the optional second argument 'option',
* the first day in a week can be changed (e.g. 0 = Sunday). The returned numbers weeks are
* not ISO 8601 compliant anymore, but can be more appropriate for display in a calendar. The
* argument can be a number, a 'scout.Locale' or a 'scout.DateFormat' object.
*/
weekInYear(date: Date, option?: number | Locale | DateFormat): number {
if (!date) {
return undefined;
}
let firstDayOfWeekArg = 1;
if (option instanceof DateFormat) {
firstDayOfWeekArg = option.symbols.firstDayOfWeek;
} else if (option instanceof Locale) {
firstDayOfWeekArg = option.dateFormatSymbols.firstDayOfWeek;
} else if (typeof option === 'number') {
firstDayOfWeekArg = option;
}
// Thursday of current week decides the year
let thursday = dates._thursdayOfWeek(date, firstDayOfWeekArg);
// In ISO format, the week with January 4th is the first week
let jan4 = new Date(thursday.getFullYear(), 0, 4);
// If the date is before the beginning of the year, it belongs to the year before
let startJan4 = dates.firstDayOfWeek(jan4, firstDayOfWeekArg);
if (date.getTime() < startJan4.getTime()) {
jan4 = new Date(thursday.getFullYear() - 1, 0, 4);
}
// Get the Thursday of the first week, to be able to compare it to 'thursday'
let thursdayFirstWeek = dates._thursdayOfWeek(jan4, firstDayOfWeekArg);
let diffInDays = (thursday.getTime() - thursdayFirstWeek.getTime()) / 86400000;
return 1 + Math.round(diffInDays / 7);
},
/** @internal */
_thursdayOfWeek(date: Date, firstDayOfWeekArg: number): Date {
if (!date || typeof firstDayOfWeekArg !== 'number') {
return undefined;
}
let thursday = new Date(date.valueOf());
if (thursday.getDay() !== 4) { // 0 = Sun, 1 = Mon, 2 = Thu, 3 = Wed, 4 = Thu, 5 = Fri, 6 = Sat
if (thursday.getDay() < firstDayOfWeekArg) {
// go 1 week backward
thursday.setDate(thursday.getDate() - 7);
}
thursday.setDate(thursday.getDate() - thursday.getDay() + 4); // go to start of week, then add 4 to go to Thursday
}
return thursday;
},
firstDayOfWeek(date: Date, firstDayOfWeekArg: number): Date {
if (!date || typeof firstDayOfWeekArg !== 'number') {
return undefined;
}
let firstDay = new Date(date.valueOf());
if (firstDay.getDay() !== firstDayOfWeekArg) {
firstDay.setDate(firstDay.getDate() - (firstDay.getDay() + 7 - firstDayOfWeekArg) % 7);
}
return firstDay;
},
/**
* Parses a string that corresponds to one of the canonical JSON transfer formats
* and returns it as a JavaScript 'Date' object.
*
* @see JsonDate.java
*/
parseJsonDate(jsonDate: string): Date {
if (!jsonDate) {
return null;
}
let year = 1970,
month = 1,
day = 1,
hours = 0,
minutes = 0,
seconds = 0,
milliseconds = 0,
utc = false;
// Date + Time
let matches = /^\+?(\d{4,5})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3})(Z?)$/.exec(jsonDate);
if (matches !== null) {
year = parseInt(matches[1], 10);
month = parseInt(matches[2], 10);
day = parseInt(matches[3], 10);
hours = parseInt(matches[4], 10);
minutes = parseInt(matches[5], 10);
seconds = parseInt(matches[6], 10);
milliseconds = parseInt(matches[7], 10);
utc = matches[8] === 'Z';
} else {
// Date only
matches = /^\+?(\d{4,5})-(\d{2})-(\d{2})(Z?)$/.exec(jsonDate);
if (matches !== null) {
year = parseInt(matches[1], 10);
month = parseInt(matches[2], 10);
day = parseInt(matches[3], 10);
utc = matches[4] === 'Z';
} else {
// Time only
matches = /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})(Z?)$/.exec(jsonDate);
if (matches !== null) {
hours = parseInt(matches[1], 10);
minutes = parseInt(matches[2], 10);
seconds = parseInt(matches[3], 10);
milliseconds = parseInt(matches[4], 10);
utc = matches[5] === 'Z';
} else {
throw new Error('Unparseable date: ' + jsonDate);
}
}
}
let result;
if (utc) {
// UTC date
result = new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, milliseconds));
if (year < 100) { // fix "two-digit years between 1900 and 1999" logic
result.setUTCFullYear(year);
}
} else {
// local date
result = new Date(year, month - 1, day, hours, minutes, seconds, milliseconds);
if (year < 100) { // fix "two-digit years between 1900 and 1999" logic
result.setFullYear(year);
}
}
return result;
},
/**
* Converts the given date object to a JSON string. By default, the local time zone
* is used to build the result, time zone information itself is not part of the
* result. If the argument 'utc' is set to true, the result is built using the
* UTC values of the date. Such a result string is marked with a trailing 'Z' character.
*
* @see JsonDate.java
*/
toJsonDate(date: Date, utc?: boolean, includeDate?: boolean, includeTime?: boolean): string {
if (!date) {
return null;
}
if (includeDate === undefined) {
includeDate = true;
}
if (includeTime === undefined) {
includeTime = true;
}
let datePart, timePart, utcPart;
if (utc) {
// (note: month is 0-indexed)
datePart = getYearPart(date) + '-' +
strings.padZeroLeft(date.getUTCMonth() + 1, 2) + '-' +
strings.padZeroLeft(date.getUTCDate(), 2);
timePart = strings.padZeroLeft(date.getUTCHours(), 2) + ':' +
strings.padZeroLeft(date.getUTCMinutes(), 2) + ':' +
strings.padZeroLeft(date.getUTCSeconds(), 2) + '.' +
strings.padZeroLeft(date.getUTCMilliseconds(), 3);
utcPart = 'Z';
} else {
// (note: month is 0-indexed)
datePart = getYearPart(date) + '-' +
strings.padZeroLeft(date.getMonth() + 1, 2) + '-' +
strings.padZeroLeft(date.getDate(), 2);
timePart = strings.padZeroLeft(date.getHours(), 2) + ':' +
strings.padZeroLeft(date.getMinutes(), 2) + ':' +
strings.padZeroLeft(date.getSeconds(), 2) + '.' +
strings.padZeroLeft(date.getMilliseconds(), 3);
utcPart = '';
}
let result = '';
if (includeDate) {
result += datePart;
if (includeTime) {
result += ' ';
}
}
if (includeTime) {
result += timePart;
}
result += utcPart;
return result;
function getYearPart(date) {
let year = date.getFullYear();
if (year > 9999) {
return '+' + year;
}
return strings.padZeroLeft(year, 4);
}
},
toJsonDateRange(range: DateRange): JsonDateRange {
return {
from: dates.toJsonDate(range.from),
to: dates.toJsonDate(range.to)
};
},
/**
* Creates a new JavaScript Date object by parsing the given string. This method is not intended to be
* used in application code, but provides a quick way to create dates in unit tests.
*
* The format is as follows:
*
* [Year#4|5]-[Month#2]-[Day#2] [Hours#2]:[Minutes#2]:[Seconds#2].[Milliseconds#3][Z]
*
* The year component is mandatory, but all others are optional (starting from the beginning).
* The date is constructed using the local time zone. If the last character is 'Z', then
* the values are interpreted as UTC date.
*/
create(dateString: string): Date {
if (dateString) {
let matches = /^(\d{4,5})(?:-(\d{2})(?:-(\d{2})(?: (\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3}))?(Z?))?)?)?)?)?/.exec(dateString);
if (matches === null) {
throw new Error('Unparsable date: ' + dateString);
}
let date;
if (matches[8] === 'Z') {
date = new Date(Date.UTC(
parseInt(matches[1], 10), // fullYear
(parseInt(matches[2], 10) || 1) - 1, // month (0-indexed)
parseInt(matches[3], 10) || 1, // day of month
parseInt(matches[4], 10) || 0, // hours
parseInt(matches[5], 10) || 0, // minutes
parseInt(matches[6], 10) || 0, // seconds
parseInt(matches[7], 10) || 0 // milliseconds
));
} else {
date = new Date(
parseInt(matches[1], 10), // fullYear
(parseInt(matches[2], 10) || 1) - 1, // month (0-indexed)
parseInt(matches[3], 10) || 1, // day of month
parseInt(matches[4], 10) || 0, // hours
parseInt(matches[5], 10) || 0, // minutes
parseInt(matches[6], 10) || 0, // seconds
parseInt(matches[7], 10) || 0 // milliseconds
);
}
return date;
}
return undefined;
},
/**
* Returns a new Date. Use this function in place of <code>new Date();</code> in your productive code
* when you want to provide a fixed date instead of the system time/date for unit tests. In your unit test
* you can replace this function with a function that provides a fixed date. Don't forget to restore the
* original function when you cleanup/tear-down the test.
*/
newDate(): Date {
return new Date();
},
format(date: Date, locale: Locale, pattern?: string): string {
let dateFormat = new DateFormat(locale, pattern);
return dateFormat.format(date);
},
/**
* Uses the default date and time format patterns from the locale to format the given date.
*/
formatDateTime(date: Date, locale: Locale): string {
let dateFormat = new DateFormat(locale, locale.dateFormatPatternDefault + ' ' + locale.timeFormatPatternDefault);
return dateFormat.format(date);
},
/**
* Formats the given time duration as follows:
* [Days] day(s) [Hours]h [Minutes]m [Seconds]s
* or, if milliseconds are included:
* [Days] day(s) [Hours]h [Minutes]m [Seconds].[Milliseconds]s
*
* @param durationInMilliseconds
* The time duration in milliseconds
* @param includeMilliseconds
* If this flag is true, the milliseconds part is included in the text
* @param locale
* The locale that should be used to format the text
* @returns The time duration as formatted text
* @see DateTimePeriodFormatter.java
*/
formatDuration(durationInMilliseconds: number, includeMilliseconds: boolean, locale: Locale): string {
// KEEP IN SYNC WITH org.eclipse.scout.rt.platform.util.date.DateTimePeriodFormatter.formatDuration(java.time.Duration, boolean)
if (durationInMilliseconds === null || durationInMilliseconds === undefined) {
return null;
}
if (durationInMilliseconds < 0) {
durationInMilliseconds = 0;
}
let time = durationInMilliseconds;
let milliseconds = time % 1000;
time = Math.floor(time / 1000);
let seconds = time % 60;
time = Math.floor(time / 60);
let minutes = time % 60;
time = Math.floor(time / 60);
let hours = time % 24;
let days = Math.floor(time / 24);
// the highest non-zero unit can be displayed without padding-zeros
// all lower units are displayed with padding zeroes, even if they are zero
// this is done so that two different periods formatted by this algorithm can be easily compared when stacked on top of one another
// because the digits of the same time-unit always appear in the same position of the text
let showDays = days > 0;
let showHours = showDays || hours > 0;
let showMinutes = showHours || minutes > 0;
let result = '';
if (showDays) {
result = days + ' ' + (days > 1 ? texts.resolveText('${textKey:ui.DaysUnit}', locale.languageTag) : texts.resolveText('${textKey:ui.DayUnit}', locale.languageTag)) + ' ';
}
if (showHours) {
// because the days-part has different lengths ('2 days' vs '1 day'), there is no need to pad the hours-part with zeros
// as the digits of the day cannot possibly align anyway. Make the text more readable by leaving out this leading zero.
result += hours + 'h ';
}
if (showMinutes) {
result += (showHours ? strings.padZeroLeft(minutes, 2) : minutes) + 'm ';
}
// seconds are always shown
result += (showMinutes ? strings.padZeroLeft(seconds, 2) : seconds);
if (includeMilliseconds) {
let decimalSeparator = locale.decimalFormatSymbols.decimalSeparator;
result += decimalSeparator + strings.padZeroLeft(milliseconds, 3);
}
result += 's';
return result;
},
compare(a: Date, b: Date): number {
if (!a && !b) {
return 0;
}
if (!a) {
return -1;
}
if (!b) {
return 1;
}
let diff = a.getTime() - b.getTime();
if (diff < -1) {
return -1;
}
if (diff > 1) {
return 1;
}
return diff;
},
equals(a: Date, b: Date): boolean {
return dates.compare(a, b) === 0;
},
/**
* This combines a date and time, passed as date objects to one object with the date part of param date and the time part of param time.
* <p>
* If time is omitted, 00:00:00 is used as time part.<br>
* If date is omitted, 1970-01-01 is used as date part independent of the time zone, means it is 1970-01-01 in every time zone.
*/
combineDateTime(date: Date, time?: Date): Date {
let newDate = new Date();
newDate.setHours(0, 0, 0, 0); // set time part to zero in local time!
newDate.setFullYear(1970, 0, 1); // make sure local time has no effect on date (if date is omitted it has to be 1970-01-01)
if (date) {
newDate.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
}
if (time) {
newDate.setHours(scout.nvl(time.getHours(), 0));
newDate.setMinutes(scout.nvl(time.getMinutes(), 0));
newDate.setSeconds(scout.nvl(time.getSeconds(), 0));
newDate.setMilliseconds(scout.nvl(time.getMilliseconds(), 0));
}
return newDate;
},
/**
* Returns <code>true</code> if the given year is a leap year, i.e if february 29 exists in that year.
*/
isLeapYear(year: number): boolean {
if (year === undefined || year === null) {
return false;
}
let date = new Date(year, 1, 29);
return date.getDate() === 29;
},
/**
* Returns the given date with time set to midnight (hours, minutes, seconds, milliseconds = 0).
*
* @param date (required)
* The date to truncate.
* @param [createCopy] (optional)
* If this flag is true, a copy of the given date is returned (the input date is not
* altered). If the flag is false, the given object is changed and then returned.
* The default value for this flag is "true".
*/
trunc(date: Date, createCopy?: boolean): Date {
if (date) {
if (scout.nvl(createCopy, true)) {
date = new Date(date.getTime());
}
date.setHours(0, 0, 0, 0); // clear time
}
return date;
},
/**
* Returns the given date with time set to midnight (hours, minutes, seconds, milliseconds = 0).
*
* @param date
* The date to truncate.
* @param [minutesResolution] default is 30
* The amount of minutes added to every full hour XX:00 until > XX+1:00. The given date will rounded up to the next valid time.
* e.g. time:15:05, resolution 40 -> 15:40
* time: 15:41 resolution 40 -> 16:00
* @param [createCopy]
* If this flag is true, a copy of the given date is returned (the input date is not
* altered). If the flag is false, the given object is changed and then returned.
* The default value for this flag is "true".
*/
ceil(date: Date, minutesResolution?: number, createCopy?: boolean): Date {
let h, m, minResolution = scout.nvl(minutesResolution, 30);
if (date) {
if (scout.nvl(createCopy, true)) {
date = new Date(date.getTime());
}
date.setSeconds(0, 0); // clear seconds and millis
m = ((date.getMinutes() + minResolution) / minResolution) * minResolution;
h = date.getHours();
if (m >= 60) {
h++;
m = 0;
}
if (h > 23) {
h = 0;
date.setDate(date.getDate() + 1);
}
date.setHours(h, m);
}
return date;
}
};