UNPKG

hyperformula

Version:

HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas

331 lines (329 loc) 10.7 kB
"use strict"; exports.__esModule = true; exports.DateTimeHelper = void 0; exports.instanceOfSimpleDate = instanceOfSimpleDate; exports.instanceOfSimpleTime = instanceOfSimpleTime; exports.maxDate = void 0; exports.numberToSimpleTime = numberToSimpleTime; exports.offsetMonth = offsetMonth; exports.roundToNearestSecond = roundToNearestSecond; exports.timeToNumber = timeToNumber; exports.toBasisEU = toBasisEU; exports.truncateDayInMonth = truncateDayInMonth; var _InterpreterValue = require("./interpreter/InterpreterValue"); /** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ const numDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const prefSumDays = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; const SECONDS_PER_MINUTE = 60; const MINUTES_PER_HOUR = 60; const HOURS_PER_DAY = 24; // eslint-disable-next-line @typescript-eslint/no-explicit-any function instanceOfSimpleDate(obj) { if (obj && (typeof obj === 'object' || typeof obj === 'function')) { return 'year' in obj && typeof obj.year === 'number' && 'month' in obj && typeof obj.month === 'number' && 'day' in obj && typeof obj.day === 'number'; } else { return false; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any function instanceOfSimpleTime(obj) { if (obj && (typeof obj === 'object' || typeof obj === 'function')) { return 'hours' in obj && typeof obj.hours === 'number' && 'minutes' in obj && typeof obj.minutes === 'number' && 'seconds' in obj && typeof obj.seconds === 'number'; } else { return false; } } const maxDate = exports.maxDate = { year: 9999, month: 12, day: 31 }; class DateTimeHelper { constructor(config) { this.config = config; this.minDateAbsoluteValue = this.dateToNumberFromZero(config.nullDate); this.maxDateValue = this.dateToNumber(maxDate); this.leapYear1900 = config.leapYear1900; // code below fixes epochYearStart while being leapYear1900 sensitive // if nullDate is earlier than fateful 28 Feb 1900 and 1900 is not supposed to be leap year, then we should // add two days (this is the config default) // otherwise only one day if (!this.leapYear1900 && 0 <= this.dateToNumber({ year: 1900, month: 2, day: 28 })) { this.epochYearZero = this.numberToSimpleDate(2).year; } else { this.epochYearZero = this.numberToSimpleDate(1).year; } this.parseDateTime = config.parseDateTime; } getWithinBounds(dayNumber) { return dayNumber <= this.maxDateValue && dayNumber >= 0 ? dayNumber : undefined; } dateStringToDateNumber(dateTimeString) { const { dateTime, dateFormat = '', timeFormat = '' } = this.parseDateTimeFromConfigFormats(dateTimeString); if (dateTime === undefined) { return undefined; } if (instanceOfSimpleTime(dateTime)) { if (instanceOfSimpleDate(dateTime)) { return new _InterpreterValue.DateTimeNumber(timeToNumber(dateTime) + this.dateToNumber(dateTime), dateFormat + ' ' + timeFormat); } else { return new _InterpreterValue.TimeNumber(timeToNumber(dateTime), timeFormat); } } else { if (instanceOfSimpleDate(dateTime)) { return new _InterpreterValue.DateNumber(this.dateToNumber(dateTime), dateFormat); } else { return 0; } } } parseDateTimeFromConfigFormats(dateTimeString) { return this.parseDateTimeFromFormats(dateTimeString, this.config.dateFormats, this.config.timeFormats); } getNullYear() { return this.config.nullYear; } getEpochYearZero() { return this.epochYearZero; } isValidDate(date) { if (isNaN(date.year) || isNaN(date.month) || isNaN(date.day)) { return false; } else if (date.day !== Math.round(date.day) || date.month !== Math.round(date.month) || date.year !== Math.round(date.year)) { return false; } else if (date.year < 1582) { // Gregorian calendar start return false; } else if (date.month < 1 || date.month > 12) { return false; } else if (date.day < 1) { return false; } else if (this.isLeapYear(date.year) && date.month === 2) { return date.day <= 29; } else { return date.day <= numDays[date.month - 1]; } } dateToNumber(date) { return this.dateToNumberFromZero(date) - this.minDateAbsoluteValue; } relativeNumberToAbsoluteNumber(arg) { return arg + this.minDateAbsoluteValue - (this.leapYear1900 ? 1 : 0); } numberToSimpleDate(arg) { const dateNumber = Math.floor(arg) + this.minDateAbsoluteValue; let year = Math.floor(dateNumber / 365.2425); if (this.dateToNumberFromZero({ year: year + 1, month: 1, day: 1 }) <= dateNumber) { year++; } else if (this.dateToNumberFromZero({ year: year - 1, month: 1, day: 1 }) > dateNumber) { year--; } const dayOfYear = dateNumber - this.dateToNumberFromZero({ year, month: 1, day: 1 }); const month = dayToMonth(dayOfYear - (this.isLeapYear(year) && dayOfYear >= 59 ? 1 : 0)); const day = dayOfYear - prefSumDays[month] - (this.isLeapYear(year) && month > 1 ? 1 : 0); return { year, month: month + 1, day: day + 1 }; } numberToSimpleDateTime(arg) { const time = numberToSimpleTime(arg % 1); const carryDays = Math.floor(time.hours / HOURS_PER_DAY); time.hours = time.hours % HOURS_PER_DAY; const date = this.numberToSimpleDate(Math.floor(arg) + carryDays); return Object.assign(Object.assign({}, date), time); } leapYearsCount(year) { return Math.floor(year / 4) - Math.floor(year / 100) + Math.floor(year / 400) + (this.config.leapYear1900 && year >= 1900 ? 1 : 0); } daysInMonth(year, month) { if (this.isLeapYear(year) && month === 2) { return 29; } else { return numDays[month - 1]; } } endOfMonth(date) { return { year: date.year, month: date.month, day: this.daysInMonth(date.year, date.month) }; } toBasisUS(start, end) { if (start.day === 31) { start.day = 30; } if (start.day === 30 && end.day === 31) { end.day = 30; } if (start.month === 2 && start.day === this.daysInMonth(start.year, start.month)) { start.day = 30; if (end.month === 2 && end.day === this.daysInMonth(end.year, end.month)) { end.day = 30; } } return [start, end]; } yearLengthForBasis(start, end) { if (start.year !== end.year) { if (start.year + 1 !== end.year || start.month < end.month || start.month === end.month && start.day < end.day) { // this is true IFF at least one year of gap between dates return (this.leapYearsCount(end.year) - this.leapYearsCount(start.year - 1)) / (end.year - start.year + 1) + 365; } if (this.countLeapDays(end) !== this.countLeapDays({ year: start.year, month: start.month, day: start.day - 1 })) { return 366; } else { return 365; } } if (this.isLeapYear(start.year)) { return 366; } else { return 365; } } parseSingleFormat(dateString, dateFormat, timeFormat) { const dateTime = this.parseDateTime(dateString, dateFormat, timeFormat); if (instanceOfSimpleDate(dateTime)) { if (dateTime.year >= 0 && dateTime.year < 100) { if (dateTime.year < this.getNullYear()) { dateTime.year += 2000; } else { dateTime.year += 1900; } } if (!this.isValidDate(dateTime)) { return undefined; } } return dateTime; } parseDateTimeFromFormats(dateTimeString, dateFormats, timeFormats) { const dateFormatsArray = dateFormats.length === 0 ? [undefined] : dateFormats; const timeFormatsArray = timeFormats.length === 0 ? [undefined] : timeFormats; for (const dateFormat of dateFormatsArray) { for (const timeFormat of timeFormatsArray) { const dateTime = this.parseSingleFormat(dateTimeString, dateFormat, timeFormat); if (dateTime !== undefined) { return { dateTime, timeFormat, dateFormat }; } } } return {}; } countLeapDays(date) { if (date.month > 2 || date.month === 2 && date.day >= 29) { return this.leapYearsCount(date.year); } else { return this.leapYearsCount(date.year - 1); } } dateToNumberFromZero(date) { return 365 * date.year + prefSumDays[date.month - 1] + date.day - 1 + (date.month <= 2 ? this.leapYearsCount(date.year - 1) : this.leapYearsCount(date.year)); } isLeapYear(year) { if (year % 4) { return false; } else if (year % 100) { return true; } else if (year % 400) { return year === 1900 && this.config.leapYear1900; } else { return true; } } } exports.DateTimeHelper = DateTimeHelper; function dayToMonth(dayOfYear) { let month = 0; if (prefSumDays[month + 6] <= dayOfYear) { month += 6; } if (prefSumDays[month + 3] <= dayOfYear) { month += 3; } if (prefSumDays[month + 2] <= dayOfYear) { month += 2; } else if (prefSumDays[month + 1] <= dayOfYear) { month += 1; } return month; } function offsetMonth(date, offset) { const totalM = 12 * date.year + date.month - 1 + offset; return { year: Math.floor(totalM / 12), month: totalM % 12 + 1, day: date.day }; } function truncateDayInMonth(date) { return { year: date.year, month: date.month, day: Math.min(date.day, numDays[date.month - 1]) }; } function roundToNearestSecond(arg) { return Math.round(arg * 3600 * 24) / (3600 * 24); } function roundToEpsilon(arg, epsilon = 1) { return Math.round(arg * epsilon) / epsilon; } // Note: The result of this function might be { hours = 24, minutes = 0, seconds = 0 } if arg < 1 but arg ≈ 1 function numberToSimpleTime(arg) { const argAsSeconds = arg * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE; const seconds = roundToEpsilon(argAsSeconds % SECONDS_PER_MINUTE, 100000) % SECONDS_PER_MINUTE; const argAsMinutes = (argAsSeconds - seconds) / SECONDS_PER_MINUTE; const minutes = Math.round(argAsMinutes % MINUTES_PER_HOUR) % MINUTES_PER_HOUR; const argAsHours = (argAsMinutes - minutes) / MINUTES_PER_HOUR; const hours = Math.round(argAsHours); return { hours, minutes, seconds }; } function timeToNumber(time) { return ((time.seconds / 60 + time.minutes) / 60 + time.hours) / 24; } function toBasisEU(date) { return { year: date.year, month: date.month, day: Math.min(30, date.day) }; }