From c3af97dc2fa77093d0868c4bf5b9ddc58ae37d88 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 14:46:08 +0000 Subject: [PATCH 1/6] Add GESTEP function (HF-481) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + docs/guide/list-of-differences.md | 1 + src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/idID.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + .../functionMetadata/categories/engineering.ts | 7 +++++++ src/interpreter/plugin/DeltaPlugin.ts | 13 +++++++++++++ src/parser/LexerConfig.ts | 2 +- 22 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 787c642ad9..cdad9e7bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) +- Added a new function: `GESTEP`. ### Changed diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a109f82616..3385580f4f 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -127,3 +127,4 @@ A few of the rows above share a root cause worth stating once: - **Rounding toward zero, not down.** `INT` discards the fractional part rather than rounding toward negative infinity, so it differs from Excel and Google Sheets for negative input only. `ROUNDDOWN`/`ROUNDUP` are unaffected — they are defined in terms of zero in all three. - **`ISEVEN`/`ISODD` do not truncate.** They test the remainder of the value as given, so a value with a fractional part returns `FALSE` from *both*. Excel and Google Sheets truncate to an integer first, so exactly one of the two is always `TRUE`. - **`CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1.** Excel and Google Sheets switch the negative-number rounding direction for any non-zero `mode`. +- **`GESTEP` coerces a boolean argument to a number**, like `DELTA`: `=GESTEP(TRUE, 0)` returns `1`, and a cell holding `FALSE` counts as `0`. Microsoft Excel returns `#VALUE!` for a boolean literal and for a boolean cell. diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..f0cbc3e157 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'CONFIDENCE.T', DEVSQ: 'DEVSQ', GEOMEAN: 'GEOMEAN', + GESTEP: 'GESTEP', HARMEAN: 'HARMEAN', CRITBINOM: 'CRITBINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..d80bc7eecc 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'KONFIDENST', DEVSQ: 'SAK', GEOMEAN: 'GEOMIDDELVÆRDI', + GESTEP: 'GESTEP', HARMEAN: 'HARMIDDELVÆRDI', CRITBINOM: 'KRITBINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..0e7eabdbb2 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'KONFIDENZ.T', DEVSQ: 'SUMQUADABW', GEOMEAN: 'GEOMITTEL', + GESTEP: 'GESTEP', HARMEAN: 'HARMITTEL', CRITBINOM: 'KRITBINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..6a11e06ecd 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'CONFIDENCE.T', DEVSQ: 'DEVSQ', GEOMEAN: 'GEOMEAN', + GESTEP: 'GESTEP', HARMEAN: 'HARMEAN', CRITBINOM: 'CRITBINOM', 'COVARIANCE.P': 'COVARIANCE.P', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..249208401b 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -399,6 +399,7 @@ export const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'INTERVALO.CONFIANZA.T', DEVSQ: 'DESVIA2', GEOMEAN: 'MEDIA.GEOM', + GESTEP: 'GESTEP', HARMEAN: 'MEDIA.ARMO', CRITBINOM: 'BINOM.CRIT', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..f97bc28152 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'LUOTTAMUSVÄLI.T', DEVSQ: 'OIKAISTU.NELIÖSUMMA', GEOMEAN: 'KESKIARVO.GEOM', + GESTEP: 'GESTEP', HARMEAN: 'KESKIARVO.HARM', CRITBINOM: 'BINOMIJAKAUMA.KRIT', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..9c79893d02 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'INTERVALLE.CONFIANCE.STUDENT', DEVSQ: 'SOMME.CARRES.ECARTS', GEOMEAN: 'MOYENNE.GEOMETRIQUE', + GESTEP: 'GESTEP', HARMEAN: 'MOYENNE.HARMONIQUE', CRITBINOM: 'CRITERE.LOI.BINOMIALE', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..600bfd2f4e 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'MEGBÍZHATÓSÁG.T', DEVSQ: 'SQ', GEOMEAN: 'MÉRTANI.KÖZÉP', + GESTEP: 'GESTEP', HARMEAN: 'HARM.KÖZÉP', CRITBINOM: 'KRITBINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..626ce14b2d 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'KEYAKINAN.T', DEVSQ: 'KUADRAT.SIMPANG', GEOMEAN: 'RATA.GEO', + GESTEP: 'GESTEP', HARMEAN: 'RATA.HARM', CRITBINOM: 'KRIT.BINOM', 'COVARIANCE.P': 'KOVAR.P', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..bdf949e9f0 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'CONFIDENZA.T', DEVSQ: 'DEV.Q', GEOMEAN: 'MEDIA.GEOMETRICA', + GESTEP: 'GESTEP', HARMEAN: 'MEDIA.ARMONICA', CRITBINOM: 'CRIT.BINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..d63e53e1ad 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'KONFIDENS.T', DEVSQ: 'AVVIK.KVADRERT', GEOMEAN: 'GJENNOMSNITT.GEOMETRISK', + GESTEP: 'GESTEP', HARMEAN: 'GJENNOMSNITT.HARMONISK', CRITBINOM: 'GRENSE.BINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..da80c92fd7 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'VERTROUWELIJKHEID.T', DEVSQ: 'DEV.KWAD', GEOMEAN: 'MEETK.GEM', + GESTEP: 'GESTEP', HARMEAN: 'HARM.GEM', CRITBINOM: 'CRIT.BINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..dd749dd243 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'UFNOŚĆ.T', DEVSQ: 'ODCH.KWADRATOWE', GEOMEAN: 'ŚREDNIA.GEOMETRYCZNA', + GESTEP: 'GESTEP', HARMEAN: 'ŚREDNIA.HARMONICZNA', CRITBINOM: 'PRÓG.ROZKŁAD.DWUM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..7debc3bd23 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'INT.CONFIANÇA.T', DEVSQ: 'DESVQ', GEOMEAN: 'MÉDIA.GEOMÉTRICA', + GESTEP: 'GESTEP', HARMEAN: 'MÉDIA.HARMÔNICA', CRITBINOM: 'CRIT.BINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..9cfa772633 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'ДОВЕРИТ.СТЬЮДЕНТ', DEVSQ: 'КВАДРОТКЛ', GEOMEAN: 'СРГЕОМ', + GESTEP: 'GESTEP', HARMEAN: 'СРГАРМ', CRITBINOM: 'КРИТБИНОМ', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..b862c76ebf 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'KONFIDENS.T', DEVSQ: 'KVADAVV', GEOMEAN: 'GEOMEDEL', + GESTEP: 'GESTEP', HARMEAN: 'HARMMEDEL', CRITBINOM: 'KRITBINOM', PEARSON: 'PEARSON', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..fca31844ae 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -399,6 +399,7 @@ const dictionary: RawTranslationPackage = { 'CONFIDENCE.T': 'GÜVENİLİRLİK.T', DEVSQ: 'SAPKARE', GEOMEAN: 'GEOORT', + GESTEP: 'GESTEP', HARMEAN: 'HARORT', CRITBINOM: 'KRİTİKBİNOM', PEARSON: 'PEARSON', diff --git a/src/interpreter/functionMetadata/categories/engineering.ts b/src/interpreter/functionMetadata/categories/engineering.ts index 04b49aca14..d8d0461e9b 100644 --- a/src/interpreter/functionMetadata/categories/engineering.ts +++ b/src/interpreter/functionMetadata/categories/engineering.ts @@ -129,6 +129,13 @@ export const ENGINEERING_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=DELTA(5, 5)', '=DELTA(5, 4)', '=DELTA(0)'], }, + GESTEP: { + category: 'Engineering', + shortDescription: 'Returns 1 if number is greater than or equal to step, otherwise returns 0.', + parameters: [{name: 'number', description: 'The value to compare against the step.'}, {name: 'step', description: 'The threshold value; defaults to 0 when omitted.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=GESTEP(5, 4)', '=GESTEP(3, 4)', '=GESTEP(-1)'], + }, ERF: { category: 'Engineering', shortDescription: 'Returns values of the Gaussian error integral.', diff --git a/src/interpreter/plugin/DeltaPlugin.ts b/src/interpreter/plugin/DeltaPlugin.ts index 2585c7be70..96df4e2e8d 100644 --- a/src/interpreter/plugin/DeltaPlugin.ts +++ b/src/interpreter/plugin/DeltaPlugin.ts @@ -17,6 +17,13 @@ export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypeche {argumentType: FunctionArgumentType.NUMBER, defaultValue: 0}, ] }, + 'GESTEP': { + method: 'gestep', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER}, + {argumentType: FunctionArgumentType.NUMBER, defaultValue: 0}, + ] + }, } public delta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { @@ -24,4 +31,10 @@ export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypeche (left: number, right: number) => (left === right ? 1 : 0) ) } + + public gestep(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('GESTEP'), + (num: number, step: number) => (num >= step ? 1 : 0) + ) + } } diff --git a/src/parser/LexerConfig.ts b/src/parser/LexerConfig.ts index 72fc4bb3b6..aaf0899769 100644 --- a/src/parser/LexerConfig.ts +++ b/src/parser/LexerConfig.ts @@ -92,7 +92,7 @@ export const buildLexerConfig = (config: ParserConfig): LexerConfig => { const WhiteSpace = createToken({ name: 'WhiteSpace', pattern: whitespaceTokenRegexp }) const ArrayRowSeparator = createToken({name: 'ArrayRowSep', pattern: config.arrayRowSeparator}) const ArrayColSeparator = createToken({name: 'ArrayColSep', pattern: config.arrayColumnSeparator}) - const NumberLiteral = createToken({ name: 'NumberLiteral', pattern: new RegExp(`(([${config.decimalSeparator}]\\d+)|(\\d+([${config.decimalSeparator}]\\d*)?))(e[+-]?\\d+)?`) }) + const NumberLiteral = createToken({ name: 'NumberLiteral', pattern: new RegExp(`(([${config.decimalSeparator}]\\d+)|(\\d+([${config.decimalSeparator}]\\d*)?))(e[+-]?\\d+)?`, 'i') }) const OffsetProcedureName = createToken({ name: 'OffsetProcedureName', pattern: new RegExp(offsetProcedureNameLiteral, 'i') }) let ArgSeparator: TokenType From de8fe6d6e7df8e9ad22593c6804397de468b40e4 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 14:46:25 +0000 Subject: [PATCH 2/6] docs: link the PR in the CHANGELOG entry Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdad9e7bb7..adbda43176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added support for the new license key format. A proprietary key can now grant a subset of the library: a function your key does not include evaluates to a `#LIC!` error, and the matching parts of the API throw a `LicenseCapabilityMissingError`. `getAvailableFunctions()` and `getFunctionDetails()` describe only the functions your key includes. [#1728](https://github.com/handsontable/hyperformula/pull/1728) -- Added a new function: `GESTEP`. +- Added a new function: `GESTEP`. [#1788](https://github.com/handsontable/hyperformula/pull/1788) ### Changed From a15ff35ac8459bb3e805fa0bb3e5a050eea84f08 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 6 Oct 2026 04:01:51 +0000 Subject: [PATCH 3/6] docs: record the number-literal fix in the CHANGELOG Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adbda43176..6a31a97cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,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 number literals with an uppercase exponent marker, such as `=1E5` or `=1E-3`, being rejected as a parse error; the lowercase form was already accepted. [#1788](https://github.com/handsontable/hyperformula/pull/1788) - 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) From 6903127121d842148b49546ee4360e38268514cd Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 7 Oct 2026 05:54:11 +0000 Subject: [PATCH 4/6] Make DELTA and GESTEP reject booleans and the empty text like Excel Excel returns #VALUE! for a boolean and for the empty text in these functions and accepts numbers, dates, numeric text and empty cells. Verified in Excel Online for 57 formulas; the list-of-differences entry for GESTEP is no longer needed. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + docs/guide/list-of-differences.md | 1 - src/interpreter/plugin/DeltaPlugin.ts | 111 ++++++++++++++++---------- 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a31a97cfe..542f8b4ba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,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 `DELTA` function returning a number for a boolean or an empty text argument instead of `#VALUE!` as Excel does; numeric text, dates and empty cells are still accepted. [#1788](https://github.com/handsontable/hyperformula/pull/1788) - Fixed number literals with an uppercase exponent marker, such as `=1E5` or `=1E-3`, being rejected as a parse error; the lowercase form was already accepted. [#1788](https://github.com/handsontable/hyperformula/pull/1788) - 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) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index 3385580f4f..a109f82616 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -127,4 +127,3 @@ A few of the rows above share a root cause worth stating once: - **Rounding toward zero, not down.** `INT` discards the fractional part rather than rounding toward negative infinity, so it differs from Excel and Google Sheets for negative input only. `ROUNDDOWN`/`ROUNDUP` are unaffected — they are defined in terms of zero in all three. - **`ISEVEN`/`ISODD` do not truncate.** They test the remainder of the value as given, so a value with a fractional part returns `FALSE` from *both*. Excel and Google Sheets truncate to an integer first, so exactly one of the two is always `TRUE`. - **`CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1.** Excel and Google Sheets switch the negative-number rounding direction for any non-zero `mode`. -- **`GESTEP` coerces a boolean argument to a number**, like `DELTA`: `=GESTEP(TRUE, 0)` returns `1`, and a cell holding `FALSE` counts as `0`. Microsoft Excel returns `#VALUE!` for a boolean literal and for a boolean cell. diff --git a/src/interpreter/plugin/DeltaPlugin.ts b/src/interpreter/plugin/DeltaPlugin.ts index 96df4e2e8d..638cf0ad21 100644 --- a/src/interpreter/plugin/DeltaPlugin.ts +++ b/src/interpreter/plugin/DeltaPlugin.ts @@ -1,40 +1,71 @@ -/** - * @license - * Copyright (c) 2025 Handsoncode. All rights reserved. - */ - -import {ProcedureAst} from '../../parser' -import {InterpreterState} from '../InterpreterState' -import {InterpreterValue} from '../InterpreterValue' -import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' - -export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypecheck { - public static implementedFunctions: ImplementedFunctions = { - 'DELTA': { - method: 'delta', - parameters: [ - {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.NUMBER, defaultValue: 0}, - ] - }, - 'GESTEP': { - method: 'gestep', - parameters: [ - {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.NUMBER, defaultValue: 0}, - ] - }, - } - - public delta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DELTA'), - (left: number, right: number) => (left === right ? 1 : 0) - ) - } - - public gestep(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('GESTEP'), - (num: number, step: number) => (num >= step ? 1 : 0) - ) - } -} +/** + * @license + * Copyright (c) 2025 Handsoncode. All rights reserved. + */ + +import {CellError, ErrorType} from '../../Cell' +import {ErrorMessage} from '../../error-message' +import {ProcedureAst} from '../../parser' +import {InterpreterState} from '../InterpreterState' +import {getRawValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue' +import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' + +export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions: ImplementedFunctions = { + 'DELTA': { + method: 'delta', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ] + }, + 'GESTEP': { + method: 'gestep', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ] + }, + } + + /** + * Converts an argument the way Excel does for these functions: numbers, dates, numeric text and empty cells are + * accepted, while a boolean or the empty text is #VALUE!. Errors are passed on. + */ + private strictNumber(value: InternalScalarValue): number | CellError { + if (value instanceof CellError) { + return value + } + if (typeof value === 'boolean' || value === '') { + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + } + const coerced = this.coerceScalarToNumberOrError(value) + return coerced instanceof CellError ? coerced : getRawValue(coerced) + } + + public delta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DELTA'), + (left: InternalScalarValue, right: InternalScalarValue) => { + const a = this.strictNumber(left) + const b = this.strictNumber(right) + if (a instanceof CellError) { + return a + } + return b instanceof CellError ? b : (a === b ? 1 : 0) + } + ) + } + + public gestep(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('GESTEP'), + (num: InternalScalarValue, step: InternalScalarValue) => { + const a = this.strictNumber(num) + const b = this.strictNumber(step) + if (a instanceof CellError) { + return a + } + return b instanceof CellError ? b : (a >= b ? 1 : 0) + } + ) + } +} From 1fd6d3f9e4f9777918c85039062df9b92c4634ee Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 03:55:12 +0000 Subject: [PATCH 5/6] Register GESTEP in the license capability table Co-Authored-By: Claude Sonnet 5.5 --- src/license/functionCapabilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/license/functionCapabilities.ts b/src/license/functionCapabilities.ts index a26733ec5f..6453d92bb0 100644 --- a/src/license/functionCapabilities.ts +++ b/src/license/functionCapabilities.ts @@ -85,7 +85,7 @@ const UNGROUPED_FUNCTIONS = [ 'DOLLARFR', 'DPRODUCT', 'DSTDEV', 'DSTDEVP', 'DSUM', 'DVAR', 'DVARP', 'EFFECT', 'ERF', 'ERFC', 'EXPON.DIST', 'F.DIST', 'F.DIST.RT', 'F.INV', 'F.INV.RT', 'F.TEST', 'FACT', 'FACTDOUBLE', 'FISHER', 'FISHERINV', 'FLOOR.MATH', 'FLOOR.PRECISE', 'FORMULATEXT', 'FVSCHEDULE', 'GAMMA', 'GAMMA.DIST', - 'GAMMA.INV', 'GAMMALN', 'GAUSS', 'GCD', 'GEOMEAN', 'HARMEAN', 'HEX2BIN', 'HEX2OCT', 'HYPGEOM.DIST', + 'GAMMA.INV', 'GAMMALN', 'GAUSS', 'GCD', 'GEOMEAN', 'GESTEP', 'HARMEAN', 'HEX2BIN', 'HEX2OCT', 'HYPGEOM.DIST', 'IMABS', 'IMAGINARY', 'IMARGUMENT', 'IMCONJUGATE', 'IMCOS', 'IMCOSH', 'IMCOT', 'IMCSC', 'IMCSCH', 'IMDIV', 'IMEXP', 'IMLN', 'IMLOG10', 'IMLOG2', 'IMPOWER', 'IMPRODUCT', 'IMREAL', 'IMSEC', 'IMSECH', 'IMSIN', 'IMSINH', 'IMSQRT', 'IMSUB', 'IMSUM', 'IMTAN', 'INTERVAL', 'ISBINARY', 'ISFORMULA', 'ISNONTEXT', 'ISPMT', From 04c400c85c6585c03201a388f1b50c99c490bfdf Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 04:22:51 +0000 Subject: [PATCH 6/6] Check the second argument of DELTA and GESTEP first, as Excel does Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/DeltaPlugin.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/interpreter/plugin/DeltaPlugin.ts b/src/interpreter/plugin/DeltaPlugin.ts index 638cf0ad21..949b3a1548 100644 --- a/src/interpreter/plugin/DeltaPlugin.ts +++ b/src/interpreter/plugin/DeltaPlugin.ts @@ -30,7 +30,8 @@ export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Converts an argument the way Excel does for these functions: numbers, dates, numeric text and empty cells are - * accepted, while a boolean or the empty text is #VALUE!. Errors are passed on. + * accepted, while a boolean or the empty text is #VALUE!. Errors are passed on. The second argument is checked first, + * as Excel does, so an error or a type error in it wins over one in the first argument. */ private strictNumber(value: InternalScalarValue): number | CellError { if (value instanceof CellError) { @@ -46,12 +47,12 @@ export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypeche public delta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('DELTA'), (left: InternalScalarValue, right: InternalScalarValue) => { - const a = this.strictNumber(left) const b = this.strictNumber(right) - if (a instanceof CellError) { - return a + const a = this.strictNumber(left) + if (b instanceof CellError) { + return b } - return b instanceof CellError ? b : (a === b ? 1 : 0) + return a instanceof CellError ? a : (a === b ? 1 : 0) } ) } @@ -59,12 +60,12 @@ export class DeltaPlugin extends FunctionPlugin implements FunctionPluginTypeche public gestep(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('GESTEP'), (num: InternalScalarValue, step: InternalScalarValue) => { - const a = this.strictNumber(num) const b = this.strictNumber(step) - if (a instanceof CellError) { - return a + const a = this.strictNumber(num) + if (b instanceof CellError) { + return b } - return b instanceof CellError ? b : (a >= b ? 1 : 0) + return a instanceof CellError ? a : (a >= b ? 1 : 0) } ) }