diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdba941d..c35ce36c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed - Fixed the validation of classic (25-character) license keys depending on the time zone: east of UTC, a key that expired the day before the build was released was still accepted, and west of UTC, the console message printed an expiry date one day too early. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Fixed the `EDATE` function returning the 28th of February instead of the 29th when the shifted date falls in February of a leap year (e.g. `=EDATE(DATE(2028, 1, 31), 1)` now returns `2028-02-29` instead of `2028-02-28`). [#1781](https://github.com/handsontable/hyperformula/pull/1781) - Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733) - Fixed the localized names of `VSTACK` and `HSTACK` in 14 language packs to match Microsoft Excel. [#1748](https://github.com/handsontable/hyperformula/pull/1748) - Fixed the MAXPOOL and MEDIANPOOL functions throwing an uncaught `TypeError` instead of returning the `#VALUE!` error when the range dimensions are not a whole multiple of the window size and the stride. [#1718](https://github.com/handsontable/hyperformula/pull/1718) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a109f8261..660fda0f9 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -51,6 +51,7 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google | TIMEVALUE function | =TIMEVALUE("14:31") | Type of the returned value: `CellValueDetailedType.NUMBER_TIME` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** | | EDATE function | =EDATE(DATE(2019, 7, 31), 1) | Type of the returned value: `CellValueDetailedType.NUMBER_DATE`. This is non-compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, which defines the return type as a Number, while describing it as a Date serial number through the function summary. | Cell auto-formatted as **date** | Cell auto-formatted as **regular number** | | EOMONTH function | =EOMONTH(DATE(2019, 7, 31), 1) | Type of the returned value: `CellValueDetailedType.NUMBER_DATE`. This is non-compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, which defines the return type as a Number, while describing it as a Date serial number through the function summary. | Cell auto-formatted as **date** | Cell auto-formatted as **regular number** | +| EDATE and EOMONTH results in February 1900 | =EDATE(DATE(1900, 1, 31), 1)

=EOMONTH(DATE(1900, 1, 31), 1) | With the [`leapYear1900`](/api/interfaces/configparams.md#leapyear1900) option set to `true`, February 1900 has 29 days, so both examples return 29 February 1900 (serial `60` with `nullDate` set to 1899-12-31). With the default `false`, both return 28 February 1900. | 28 February 1900 for both examples. Google Sheets does not treat 1900 as a leap year. | 28 February 1900 (serial `59`) for both examples. Excel keeps the 1900 leap day only in the serial-to-date mapping (`DAY(60)` is `29`); its month arithmetic treats February 1900 as having 28 days, so no `EDATE` or `EOMONTH` result is ever serial `60`. | | Empty cells in lookup search | =XLOOKUP(3, A1:A4, A1:A4, "NF", 0, 2)
where A1:A4 = 1, 2, (empty), 3 | Empty cells are skipped during the search. A value that is present is found even when it sits past an interspersed empty cell (exact match is gap-independent), and approximate `MATCH`/`VLOOKUP`/`HLOOKUP`/`XLOOKUP` skip empty cells — but not empty strings — when finding the lower/upper bound. Returns `3`. On an all-empty range in a binary search mode, HyperFormula returns the `if_not_found` result (never row 1). | Skips empty cells in approximate search (parity with HyperFormula). | With binary search modes (`search_mode` ±2), a range with interspersed empty cells is not strictly sorted; per Excel's documentation the result may be invalid, so a value past an empty cell is not reliably found. On an all-empty range in a binary mode, Excel returns the first row's value. | ## Built-in functions diff --git a/src/DateTimeHelper.ts b/src/DateTimeHelper.ts index aab02290c..f35c66f8d 100644 --- a/src/DateTimeHelper.ts +++ b/src/DateTimeHelper.ts @@ -176,6 +176,14 @@ export class DateTimeHelper { return {year: date.year, month: date.month, day: this.daysInMonth(date.year, date.month)} } + /** + * Clamps the day of the month so that it does not exceed the number of days in that month. + * Leap years, including the configurable 1900 leap year, are taken into account. + */ + public truncateDayInMonth(date: SimpleDate): SimpleDate { + return {year: date.year, month: date.month, day: Math.min(date.day, this.daysInMonth(date.year, date.month))} + } + public toBasisUS(start: SimpleDate, end: SimpleDate): [SimpleDate, SimpleDate] { if (start.day === 31) { start.day = 30 @@ -288,10 +296,6 @@ export function offsetMonth(date: SimpleDate, offset: number): SimpleDate { return {year: Math.floor(totalM / 12), month: totalM % 12 + 1, day: date.day} } -export function truncateDayInMonth(date: SimpleDate): SimpleDate { - return {year: date.year, month: date.month, day: Math.min(date.day, numDays[date.month - 1])} -} - export function roundToNearestSecond(arg: number): number { return Math.round(arg * 3600 * 24) / (3600 * 24) } diff --git a/src/interpreter/plugin/DateTimePlugin.ts b/src/interpreter/plugin/DateTimePlugin.ts index fd1d3fad0..a3dad74cb 100644 --- a/src/interpreter/plugin/DateTimePlugin.ts +++ b/src/interpreter/plugin/DateTimePlugin.ts @@ -12,8 +12,7 @@ import { roundToNearestSecond, SimpleDate, timeToNumber, - toBasisEU, - truncateDayInMonth + toBasisEU } from '../../DateTimeHelper' import {ErrorMessage} from '../../error-message' import {format} from '../../format/format' @@ -458,7 +457,7 @@ export class DateTimePlugin extends FunctionPlugin implements FunctionPluginType return this.runFunction(ast.args, state, this.metadata('EDATE'), (dateNumber: number, delta: number) => { const date = this.dateTimeHelper.numberToSimpleDate(dateNumber) - const newDate = truncateDayInMonth(offsetMonth(date, delta)) + const newDate = this.dateTimeHelper.truncateDayInMonth(offsetMonth(date, delta)) let ret: Maybe = this.dateTimeHelper.dateToNumber(newDate) ret = this.dateTimeHelper.getWithinBounds(ret) if (ret === undefined) {