diff --git a/CHANGELOG.md b/CHANGELOG.md index 787c642ad9..542f8b4ba9 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`. [#1788](https://github.com/handsontable/hyperformula/pull/1788) ### Changed @@ -19,6 +20,8 @@ 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) - 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/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..949b3a1548 100644 --- a/src/interpreter/plugin/DeltaPlugin.ts +++ b/src/interpreter/plugin/DeltaPlugin.ts @@ -1,27 +1,72 @@ -/** - * @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}, - ] - }, - } - - public delta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DELTA'), - (left: number, right: number) => (left === right ? 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. 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) { + 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 b = this.strictNumber(right) + const a = this.strictNumber(left) + if (b instanceof CellError) { + return b + } + return a instanceof CellError ? a : (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 b = this.strictNumber(step) + const a = this.strictNumber(num) + if (b instanceof CellError) { + return b + } + return a instanceof CellError ? a : (a >= b ? 1 : 0) + } + ) + } +} 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', 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