Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/guide/list-of-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)<br><br>=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)<br>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
Expand Down
12 changes: 8 additions & 4 deletions src/DateTimeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I measured this in Excel Online (not desktop). DAY(60) is 29, but no EDATE or EOMONTH result is ever serial 60. =EDATE(DATE(1900,1,31),1) gives 59 (28 Feb) and =EDATE(60,1) gives 89 (29 Mar), so only the February clamp is 28. Excel keeps the fake leap day in the serial-to-date mapping only. Its month arithmetic treats February 1900 as 28 days.

With leapYear1900: true and nullDate 1899-12-31, the setup the compatibility guide recommends, 16 of my 450 start/offset cases now return 60 where develop returned 59 (for example EDATE(DATE(1900,3,31),-1)). Another 161 moved closer to Excel.

I wouldn't special-case 1900. EOMONTH on develop already returns 60, and DATE(1900,2,29) is valid in that mode. A row in list-of-differences.md covering both functions should be enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, agreed on all points. I've kept the code as is, reworded the description so it no longer claims the 1900 result matches Excel, and added a row to list-of-differences.md covering both EDATE and EOMONTH under leapYear1900. The Excel values in that row are from your Excel Online measurement, so let me know if you'd rather phrase them differently.

I also rebased both branches onto develop to clear a changelog conflict; the earlier browser-test failures were the VERSION specs from the paired tests branch lagging behind the HF-307 test updates, not this change.

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
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 2 additions & 3 deletions src/interpreter/plugin/DateTimePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
roundToNearestSecond,
SimpleDate,
timeToNumber,
toBasisEU,
truncateDayInMonth
toBasisEU
} from '../../DateTimeHelper'
import {ErrorMessage} from '../../error-message'
import {format} from '../../format/format'
Expand Down Expand Up @@ -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<number> = this.dateTimeHelper.dateToNumber(newDate)
ret = this.dateTimeHelper.getWithinBounds(ret)
if (ret === undefined) {
Expand Down
Loading