kk-date
Version:
kk-date is a fastest JavaScript library that parses, validations, manipulates, and displays dates and times. If you use Moment.js or Day.js already you can easily use kk-date.
728 lines (662 loc) • 23.3 kB
JavaScript
const {
iso6391_languages,
default_en_day_number,
day_numbers,
cache_ttl,
month_numbers,
timeInMilliseconds,
format_types,
cached_dateTimeFormat,
timezone_cache,
timezone_check_cache,
timezone_abbreviation_cache,
long_timezone_cache,
timezone_formatter_cache,
global_config,
systemTimezone,
cached_converter_int,
converter_results_cache,
cached_dateTimeFormat_with_locale,
} = require('./constants');
const months = {};
const days = {};
for (const key in iso6391_languages) {
const month_long = new Intl.DateTimeFormat(key, { month: 'long' });
const month_short = new Intl.DateTimeFormat(key, { month: 'short' });
const day_long = new Intl.DateTimeFormat(key, { weekday: 'long' });
const day_short = new Intl.DateTimeFormat(key, { weekday: 'short' });
for (let i = 1; i < 13; i++) {
const date = new Date(2021, i, '0');
months[month_long.format(date).toLowerCase()] = i;
months[month_short.format(date).toLowerCase()] = i;
}
for (let i = 1; i < 8; i++) {
const date = new Date(2021, 1, i);
const number = parseInt(default_en_day_number.format(date), 10);
days[day_long.format(date).toLowerCase()] = number;
days[day_short.format(date).toLowerCase()] = number;
}
}
/**
* if valid will be turn english month name
*
* @param {string} monthName
* @returns {string|Boolean}
*/
function isValidMonth(monthName) {
const monthNameLower = monthName.toLowerCase();
return months[monthNameLower] ? month_numbers[months[monthNameLower]] : false;
}
/**
* if valid will be turn english day name
*
* @param {string} dayname
* @returns {string|Boolean}
*/
function isValidDayName(dayname) {
const dayNameLower = dayname.toLowerCase();
return days[dayNameLower] ? day_numbers[days[dayNameLower]] : false;
}
/**
* Enhanced timezone offset calculation with DST support
*
* @param {string} timezone - IANA timezone identifier
* @param {Date} date - Date for which to calculate offset (defaults to current date)
* @returns {number} - Offset in milliseconds
*/
function getTimezoneOffset(timezone, date = new Date()) {
try {
// Validate timezone
checkTimezone(timezone);
// Check cache first
const cacheKey = `${timezone}_${date.getTime()}`;
if (timezone_cache.has(cacheKey)) {
const { offset, timestamp } = timezone_cache.get(cacheKey);
if (date.getTime() - timestamp < cache_ttl) {
return offset;
}
}
let formatter;
if (long_timezone_cache.has(timezone)) {
formatter = long_timezone_cache.get(timezone);
} else {
formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
timeZoneName: 'longOffset',
});
long_timezone_cache.set(timezone, formatter);
}
// Get timezone offset using Intl.DateTimeFormat with timeZoneName
const offsetPart = formatter.formatToParts(date).find((part) => part.type === 'timeZoneName');
if (!offsetPart) {
throw new Error('Could not determine timezone offset');
}
// Parse offset like "GMT-05:00" or "GMT+08:00"
const offsetStr = offsetPart.value.replace('GMT', '');
const isNegative = offsetStr.startsWith('-');
const timeStr = offsetStr.replace(/[+-]/, '');
const [hours, minutes = '0'] = timeStr.split(':').map(Number);
const totalMinutes = hours * 60 + minutes;
// For timezone conversion, we need the offset from UTC to the target timezone
// GMT-05:00 means the timezone is 5 hours behind UTC, so offset should be -5
// GMT+08:00 means the timezone is 8 hours ahead of UTC, so offset should be +8
const offsetMs = (isNegative ? -totalMinutes : totalMinutes) * 60 * 1000;
// Cache the result
timezone_cache.set(cacheKey, {
offset: offsetMs,
timestamp: date.getTime(),
});
return offsetMs;
} catch {
throw new Error(`Invalid timezone: ${timezone}`);
}
}
/**
* Check if timezone is valid
*
* @param {string} timezone - IANA timezone identifier
* @returns {boolean}
*/
function checkTimezone(timezone) {
try {
if (timezone_check_cache.has(timezone)) {
return true;
}
// Test if timezone is valid by trying to format a date
new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format(new Date());
timezone_check_cache.set(timezone, true);
return true;
} catch {
throw new Error(`Invalid timezone: ${timezone}`);
}
}
/**
* Get timezone information including DST status
*
* @param {string} timezone - IANA timezone identifier
* @param {Date} date - Date for which to get info (defaults to current date)
* @returns {object} - Timezone information
*/
function getTimezoneInfo(timezone, date = new Date()) {
try {
checkTimezone(timezone);
// Get current offset
const currentOffset = getTimezoneOffset(timezone, date);
// Get offset for same date in winter (to detect DST)
const winterDate = new Date(date.getFullYear(), 0, 1); // January 1st
const winterOffset = getTimezoneOffset(timezone, winterDate);
// Get offset for same date in summer (to detect DST)
const summerDate = new Date(date.getFullYear(), 6, 1); // July 1st
const summerOffset = getTimezoneOffset(timezone, summerDate);
// Determine if DST is active
const isDST = currentOffset !== winterOffset;
let formatter;
if (timezone_abbreviation_cache.has(timezone)) {
formatter = timezone_abbreviation_cache.get(timezone);
} else {
formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
timeZoneName: 'short',
});
timezone_abbreviation_cache.set(timezone, formatter);
}
// Get timezone abbreviation
const abbreviation = formatter.formatToParts(date).find((part) => part.type === 'timeZoneName')?.value || timezone;
return {
timezone,
offset: currentOffset,
isDST,
abbreviation,
standardOffset: winterOffset,
daylightOffset: summerOffset,
};
} catch (error) {
throw new Error(`Failed to get timezone info for ${timezone}: ${error.message}`);
}
}
/**
* Enhanced timezone parsing with automatic DST detection
*
* @param {KkDate} kkDate - KkDate instance
* @param {boolean} is_init - Whether this is initial parsing
* @returns {Date} - Parsed date with timezone conversion
*/
function parseWithTimezone(kkDate) {
// If this is a .tz() call, convert to target timezone
if (kkDate.temp_config.timezone) {
const targetTimezone = kkDate.temp_config.timezone;
// Special handling for UTC - return original UTC time
if (targetTimezone === 'UTC') {
return kkDate.date;
}
// For .tz() calls, we don't change the date object
// The timezone information is stored in temp_config.timezone
// and the format function will handle the display conversion
return kkDate.date;
}
// Constructor call - reinterpret input in global timezone if:
// 1. Global timezone is set and different from system timezone
// 2. Input is not ISO8601 UTC timestamp
// 3. Global timezone will be used for formatting (not just UTC display)
// 4. Input is not 'now' (current time should not be reinterpreted)
const globalTimezone = global_config.timezone;
if (globalTimezone && globalTimezone !== systemTimezone && globalTimezone !== 'UTC' && kkDate.detected_format !== 'ISO8601' && kkDate.detected_format !== 'now' && kkDate.detected_format !== 'Xx' && kkDate.detected_format !== 'kkDate') {
// Reinterpret the input as being in global timezone
const systemOffset = getTimezoneOffset(systemTimezone, kkDate.date);
const globalOffset = getTimezoneOffset(globalTimezone, kkDate.date);
const offsetDiff = globalOffset - systemOffset;
// Adjust the date to represent the same clock time in global timezone
const adjustedTime = kkDate.date.getTime() - offsetDiff;
return new Date(adjustedTime);
}
return kkDate.date;
}
/**
* Convert date to specific timezone
*
* @param {Date} date - Date to convert
* @param {string} targetTimezone - Target timezone
* @param {string} sourceTimezone - Source timezone (optional, defaults to user timezone)
* @returns {Date} - Converted date
*/
function convertToTimezone(date, targetTimezone, sourceTimezone = global_config.userTimezone) {
try {
checkTimezone(targetTimezone);
checkTimezone(sourceTimezone);
const targetOffset = getTimezoneOffset(targetTimezone, date);
const sourceOffset = getTimezoneOffset(sourceTimezone, date);
const timezoneDiff = targetOffset - sourceOffset;
return new Date(date.getTime() + timezoneDiff);
} catch (error) {
throw new Error(`Failed to convert timezone: ${error.message}`);
}
}
/**
* Get all available timezones (if supported by the environment)
*
* @returns {string[]} - Array of available timezone identifiers
*/
function getAvailableTimezones() {
try {
// This is a fallback method - not all environments support this
const commonTimezones = [
'UTC',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Istanbul',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Kolkata',
'Australia/Sydney',
'Australia/Melbourne',
];
return commonTimezones.filter((tz) => {
try {
checkTimezone(tz);
return true;
} catch {
return false;
}
});
} catch {
return ['UTC']; // Fallback to UTC only
}
}
/**
* Check if a date is in DST for a given timezone
*
* @param {string} timezone - IANA timezone identifier
* @param {Date} date - Date to check (defaults to current date)
* @returns {boolean} - True if DST is active
*/
function isDST(timezone, date = new Date()) {
try {
const info = getTimezoneInfo(timezone, date);
return info.isDST;
} catch {
return false;
}
}
/**
* Get timezone abbreviation
*
* @param {string} timezone - IANA timezone identifier
* @param {Date} date - Date for abbreviation (defaults to current date)
* @returns {string} - Timezone abbreviation
*/
function getTimezoneAbbreviation(timezone, date = new Date()) {
try {
const info = getTimezoneInfo(timezone, date);
return info.abbreviation;
} catch {
return timezone;
}
}
/**
* absFloor function
*
* @param {number} number
* @returns {number}
*/
function absFloor(number) {
if (number < 0) {
return Math.ceil(number) || 0;
}
return Math.floor(number);
}
/**
* padZero
*
* @param {number} num
* @returns {string}
*/
const padZero = (num) => String(num).padStart(2, '0');
/**
* @description It divides the date string into parts and returns an object.
* @param {string} time
* @param {"years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"} type
* @returns {{years: number, months: number, weeks: number, days: number, hours: number, minutes: number, seconds: number, milliseconds: number, $kk_date: {milliseconds: number}, asMilliseconds: function(): number, asSeconds: function(): number, asMinutes: function(): number, asHours: function(): number, asDays: function(): number, asWeeks: function(): number, asMonths: function(): number, asYears: function(): number}}
* @example
* // Example usage:
* const result = duration(1234, 'minute');
* console.log(result);
* // Output: { years: 0, months: 0, weeks: 0, days: 0, hours: 20, minutes: 34, seconds: 0, milliseconds: 0 }
*/
function duration(time, type) {
if (!Number.isInteger(time)) {
throw new Error('Invalid time');
}
const _milliseconds = time * timeInMilliseconds[type];
const response = {
years: 0,
months: 0,
weeks: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
$kk_date: { milliseconds: 0 },
asMilliseconds: () => _milliseconds,
asSeconds: () => _milliseconds / timeInMilliseconds.seconds,
asMinutes: () => _milliseconds / timeInMilliseconds.minutes,
asHours: () => _milliseconds / timeInMilliseconds.hours,
asDays: () => _milliseconds / timeInMilliseconds.days,
asWeeks: () => _milliseconds / timeInMilliseconds.weeks,
asMonths: () => _milliseconds / timeInMilliseconds.months,
asYears: () => _milliseconds / timeInMilliseconds.years,
};
if (!time || typeof time !== 'number' || time < 0) {
throw new Error('Invalid time');
}
if (!timeInMilliseconds[type]) {
throw new Error('Invalid type');
}
response.$kk_date.milliseconds = _milliseconds;
let milliseconds = _milliseconds;
response.years = Math.floor(milliseconds / timeInMilliseconds.years);
milliseconds = milliseconds % timeInMilliseconds.years;
response.months = Math.floor(milliseconds / timeInMilliseconds.months);
milliseconds = milliseconds % timeInMilliseconds.months;
response.weeks = Math.floor(milliseconds / timeInMilliseconds.weeks);
milliseconds = milliseconds % timeInMilliseconds.weeks;
response.days = Math.floor(milliseconds / timeInMilliseconds.days);
milliseconds = milliseconds % timeInMilliseconds.days;
response.hours = Math.floor(milliseconds / timeInMilliseconds.hours);
milliseconds = milliseconds % timeInMilliseconds.hours;
response.minutes = Math.floor(milliseconds / timeInMilliseconds.minutes);
milliseconds = milliseconds % timeInMilliseconds.minutes;
response.seconds = Math.floor(milliseconds / timeInMilliseconds.seconds);
milliseconds = milliseconds % timeInMilliseconds.seconds;
response.milliseconds = milliseconds;
return response;
}
/**
* @description It formats the date with the locale and template.
* @param {KkDate} orj_this
* @param {string} template
* @returns {Intl.DateTimeFormat}
*/
function dateTimeFormat(orj_this, template) {
const tempLocale = orj_this.temp_config.locale;
// For timestamp inputs with configured timezone, use timezone-aware formatting
if (orj_this.detected_format === 'Xx' && global_config.timezone && global_config.timezone !== 'UTC') {
const locale = tempLocale || global_config.locale;
const timezone = global_config.timezone;
if (template === format_types.dddd) {
if (cached_dateTimeFormat_with_locale.dddd[`${locale}_${timezone}`]) {
return cached_dateTimeFormat_with_locale.dddd[`${locale}_${timezone}`];
}
cached_dateTimeFormat_with_locale.dddd[`${locale}_${timezone}`] = {
value: new Intl.DateTimeFormat(locale, { weekday: 'long', timeZone: timezone }),
id: `${locale}_${timezone}_dddd`,
};
return cached_dateTimeFormat_with_locale.dddd[`${locale}_${timezone}`];
}
if (template === format_types.ddd) {
if (cached_dateTimeFormat_with_locale.ddd[`${locale}_${timezone}`]) {
return cached_dateTimeFormat_with_locale.ddd[`${locale}_${timezone}`];
}
cached_dateTimeFormat_with_locale.ddd[`${locale}_${timezone}`] = {
value: new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: timezone }),
id: `${locale}_${timezone}_ddd`,
};
return cached_dateTimeFormat_with_locale.ddd[`${locale}_${timezone}`];
}
if (template === format_types.MMMM) {
if (cached_dateTimeFormat_with_locale.MMMM[`${locale}_${timezone}`]) {
return cached_dateTimeFormat_with_locale.MMMM[`${locale}_${timezone}`];
}
cached_dateTimeFormat_with_locale.MMMM[`${locale}_${timezone}`] = {
value: new Intl.DateTimeFormat(locale, { month: 'long', timeZone: timezone }),
id: `${locale}_${timezone}_MMMM`,
};
return cached_dateTimeFormat_with_locale.MMMM[`${locale}_${timezone}`];
}
if (template === format_types.MMM) {
if (cached_dateTimeFormat_with_locale.MMM[`${locale}_${timezone}`]) {
return cached_dateTimeFormat_with_locale.MMM[`${locale}_${timezone}`];
}
cached_dateTimeFormat_with_locale.MMM[`${locale}_${timezone}`] = {
value: new Intl.DateTimeFormat(locale, { month: 'short', timeZone: timezone }),
id: `${locale}_${timezone}_MMM`,
};
return cached_dateTimeFormat_with_locale.MMM[`${locale}_${timezone}`];
}
}
if (tempLocale) {
return { value: cached_dateTimeFormat.temp[template][tempLocale], id: `${orj_this.temp_config.locale}_0` };
}
if (template === format_types.dddd) {
return { value: cached_dateTimeFormat.dddd, id: '1' };
}
if (template === format_types.ddd) {
return { value: cached_dateTimeFormat.ddd, id: '2' };
}
if (template === format_types.MMMM) {
return { value: cached_dateTimeFormat.MMMM, id: '3' };
}
if (template === format_types.MMM) {
return { value: cached_dateTimeFormat.MMM, id: '4' };
}
throw new Error('unkown template for dateTimeFormat !');
}
/**
* date converter - OPTIMIZED VERSION
*
* @param {Date} date
* @param {Array} to
* @param {object} [options={pad: true, isUTC: false, detectedFormat: null}]
* @returns {Object}
*/
function converter(date, to, options = { pad: true }) {
const shouldPad = options.pad !== false;
const isUTC = options.isUTC || false;
const detectedFormat = options.detectedFormat || null;
const orj_this = options.orj_this;
// Determine timezone for formatting
let targetTimezone = null;
if (orj_this) {
targetTimezone = orj_this.temp_config.timezone || global_config.timezone;
}
// Create cache key for this specific conversion
const timestamp = date.getTime();
const cacheKey = `${timestamp}_${to.join(',')}_${shouldPad}_${isUTC}_${targetTimezone || 'none'}_${detectedFormat || 'none'}`;
// Check cache first
if (converter_results_cache.has(cacheKey)) {
return converter_results_cache.get(cacheKey);
}
// Limit cache size to prevent memory leaks
if (converter_results_cache.size > 10000) {
// Clear half of the cache when limit is reached
const keysToDelete = Array.from(converter_results_cache.keys()).slice(0, 5000);
for (const key of keysToDelete) {
converter_results_cache.delete(key);
}
}
const result = {};
// Use timezone-aware formatting if targetTimezone is specified and not UTC
// For ISO8601 UTC timestamps with .tz() calls, we also need timezone formatting
if (targetTimezone && targetTimezone !== 'UTC') {
// Use Intl.DateTimeFormat for timezone-aware formatting
let formatter = null;
const partsMap = {};
if (cached_converter_int[targetTimezone]) {
formatter = cached_converter_int[targetTimezone];
} else {
formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: targetTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
cached_converter_int[targetTimezone] = formatter;
}
const parts = formatter.formatToParts(date);
for (const part of parts) {
partsMap[part.type] = part.value;
}
const len = to.length;
for (let i = 0; i < len; i++) {
const field = to[i];
switch (field) {
case 'year':
result.year = partsMap.year;
break;
case 'month':
result.month = partsMap.month;
break;
case 'day':
result.day = partsMap.day;
break;
case 'hours':
result.hours = partsMap.hour;
break;
case 'minutes':
result.minutes = partsMap.minute;
break;
case 'seconds':
result.seconds = partsMap.second;
break;
case 'milliseconds':
result.milliseconds = shouldPad
? date.getMilliseconds() < 100
? `0${date.getMilliseconds() < 10 ? `0${date.getMilliseconds()}` : date.getMilliseconds()}`
: String(date.getMilliseconds())
: date.getMilliseconds();
break;
}
}
converter_results_cache.set(cacheKey, result);
return result;
}
// Fast path: Use direct Date methods for most cases
if (detectedFormat !== 'Xx' || !global_config.timezone || global_config.timezone === 'UTC') {
// Standard formatting - much faster
const len = to.length;
for (let i = 0; i < len; i++) {
const field = to[i];
switch (field) {
case 'year':
result.year = isUTC ? date.getUTCFullYear() : date.getFullYear();
break;
case 'month': {
const month = (isUTC ? date.getUTCMonth() : date.getMonth()) + 1;
result.month = shouldPad ? (month < 10 ? `0${month}` : String(month)) : month;
break;
}
case 'day': {
const day = isUTC ? date.getUTCDate() : date.getDate();
result.day = shouldPad ? (day < 10 ? `0${day}` : String(day)) : day;
break;
}
case 'hours': {
const hours = isUTC ? date.getUTCHours() : date.getHours();
result.hours = shouldPad ? (hours < 10 ? `0${hours}` : String(hours)) : hours;
break;
}
case 'minutes': {
const minutes = isUTC ? date.getUTCMinutes() : date.getMinutes();
result.minutes = shouldPad ? (minutes < 10 ? `0${minutes}` : String(minutes)) : minutes;
break;
}
case 'seconds': {
const seconds = isUTC ? date.getUTCSeconds() : date.getSeconds();
result.seconds = shouldPad ? (seconds < 10 ? `0${seconds}` : String(seconds)) : seconds;
break;
}
case 'milliseconds': {
const ms = isUTC ? date.getUTCMilliseconds() : date.getMilliseconds();
result.milliseconds = shouldPad ? (ms < 100 ? (ms < 10 ? `00${ms}` : `0${ms}`) : String(ms)) : ms;
break;
}
}
}
converter_results_cache.set(cacheKey, result);
return result;
}
// Timezone-aware formatting only when absolutely necessary
const useTimezone = targetTimezone || global_config.timezone;
// Cache the DateTimeFormat instance for performance
let formatter_value;
if (timezone_formatter_cache.has(useTimezone)) {
formatter_value = timezone_formatter_cache.get(useTimezone);
} else {
formatter_value = new Intl.DateTimeFormat('en-CA', {
timeZone: useTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
timezone_formatter_cache.set(useTimezone, formatter_value);
}
// Format the specific date
const parts = formatter_value.formatToParts(date);
const in_result = {};
const partsLen = parts.length;
// Optimized parts mapping
for (let i = 0; i < partsLen; i++) {
const part = parts[i];
in_result[part.type] = part.value;
}
// Process requested fields
const len = to.length;
for (let i = 0; i < len; i++) {
const field = to[i];
switch (field) {
case 'year':
result.year = in_result.year;
break;
case 'month':
result.month = shouldPad ? in_result.month : parseInt(in_result.month, 10);
break;
case 'day':
result.day = shouldPad ? in_result.day : parseInt(in_result.day, 10);
break;
case 'hours':
result.hours = shouldPad ? in_result.hour : parseInt(in_result.hour, 10);
break;
case 'minutes':
result.minutes = shouldPad ? in_result.minute : parseInt(in_result.minute, 10);
break;
case 'seconds':
result.seconds = shouldPad ? in_result.second : parseInt(in_result.second, 10);
break;
case 'milliseconds': {
const ms = date.getMilliseconds();
result.milliseconds = shouldPad ? (ms < 100 ? (ms < 10 ? `00${ms}` : `0${ms}`) : String(ms)) : ms;
break;
}
}
}
converter_results_cache.set(cacheKey, result);
return result;
}
module.exports.getTimezoneOffset = getTimezoneOffset;
module.exports.parseWithTimezone = parseWithTimezone;
module.exports.checkTimezone = checkTimezone;
module.exports.getTimezoneInfo = getTimezoneInfo;
module.exports.convertToTimezone = convertToTimezone;
module.exports.getAvailableTimezones = getAvailableTimezones;
module.exports.isDST = isDST;
module.exports.getTimezoneAbbreviation = getTimezoneAbbreviation;
module.exports.padZero = padZero;
module.exports.absFloor = absFloor;
module.exports.duration = duration;
module.exports.dateTimeFormat = dateTimeFormat;
module.exports.converter = converter;
module.exports.isValidMonth = isValidMonth;
module.exports.isValidDayName = isValidDayName;