From 503184a7da258ae6ce12b4851953728d867f8be7 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 09:52:17 +0000 Subject: [PATCH 1/5] Add RANK.EQ function and RANK alias (HF-419) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + src/i18n/languages/csCZ.ts | 2 + src/i18n/languages/daDK.ts | 2 + src/i18n/languages/deDE.ts | 2 + src/i18n/languages/enGB.ts | 2 + src/i18n/languages/esES.ts | 2 + src/i18n/languages/fiFI.ts | 2 + src/i18n/languages/frFR.ts | 2 + src/i18n/languages/huHU.ts | 2 + src/i18n/languages/idID.ts | 2 + src/i18n/languages/itIT.ts | 2 + src/i18n/languages/nbNO.ts | 2 + src/i18n/languages/nlNL.ts | 2 + src/i18n/languages/plPL.ts | 2 + src/i18n/languages/ptPT.ts | 2 + src/i18n/languages/ruRU.ts | 2 + src/i18n/languages/svSE.ts | 2 + src/i18n/languages/trTR.ts | 2 + .../categories/statistical.ts | 7 ++++ src/interpreter/plugin/MedianPlugin.ts | 40 +++++++++++++++++++ 20 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b68c77419b..778260a19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,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 new functions: RANK.EQ and its alias RANK. ### Changed diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..a1dfd82f48 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'LARGE', SMALL: 'SMALL', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..8ac788fb9d 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMAGTAN', LARGE: 'STØRSTE', SMALL: 'MINDSTE', + 'RANK.EQ': 'PLADS.LIGE', + RANK: 'PLADS', PERCENTILE: 'FRAKTIL', 'PERCENTILE.INC': 'FRAKTIL.MEDTAG', 'PERCENTILE.EXC': 'FRAKTIL.UDELAD', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..0322878f99 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMATAN', LARGE: 'KGRÖSSTE', SMALL: 'KKLEINSTE', + 'RANK.EQ': 'RANG.GLEICH', + RANK: 'RANG', PERCENTILE: 'QUANTIL', 'PERCENTILE.INC': 'QUANTIL.INKL', 'PERCENTILE.EXC': 'QUANTIL.EXKL', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..616968d4a8 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'LARGE', SMALL: 'SMALL', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'PERCENTILE.INC', 'PERCENTILE.EXC': 'PERCENTILE.EXC', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..02dec62253 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -387,6 +387,8 @@ export const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'K.ESIMO.MAYOR', SMALL: 'K.ESIMO.MENOR', + 'RANK.EQ': 'JERARQUIA.EQV', + RANK: 'JERARQUIA', PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..0ce4dd0adb 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'KOMPLEKSI.TAN', LARGE: 'SUURI', SMALL: 'PIENI', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'PROSENTTIPISTE', 'PERCENTILE.INC': 'PROSENTTIPISTE.SIS', 'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..f16a4333fc 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'COMPLEXE.TAN', LARGE: 'GRANDE.VALEUR', SMALL: 'PETITE.VALEUR', + 'RANK.EQ': 'EQUATION.RANG', + RANK: 'RANG', PERCENTILE: 'CENTILE', 'PERCENTILE.INC': 'CENTILE.INCLURE', 'PERCENTILE.EXC': 'CENTILE.EXCLURE', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..cb963d8bd4 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'KÉPZ.TAN', LARGE: 'NAGY', SMALL: 'KICSI', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'PERCENTILIS', 'PERCENTILE.INC': 'PERCENTILIS.TARTALMAZ', 'PERCENTILE.EXC': 'PERCENTILIS.KIZÁR', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..21f5fb5fee 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'BESAR', SMALL: 'KECIL', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'PERCENTILE.INC', 'PERCENTILE.EXC': 'PERCENTILE.EXC', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..161ee522ae 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'COMP.TAN', LARGE: 'GRANDE', SMALL: 'PICCOLO', + 'RANK.EQ': 'RANGO.UG', + RANK: 'RANGO', PERCENTILE: 'PERCENTILE', 'PERCENTILE.INC': 'INC.PERCENTILE', 'PERCENTILE.EXC': 'ESC.PERCENTILE', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..404467e801 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'N.STØRST', SMALL: 'N.MINST', + 'RANK.EQ': 'RANG.EKV', + RANK: 'RANG', PERCENTILE: 'PERSENTIL', 'PERCENTILE.INC': 'PERSENTIL.INK', 'PERCENTILE.EXC': 'PERSENTIL.EKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..c4a1568d81 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'C.TAN', LARGE: 'GROOTSTE', SMALL: 'KLEINSTE', + 'RANK.EQ': 'RANG.GELIJK', + RANK: 'RANG', PERCENTILE: 'PERCENTIEL', 'PERCENTILE.INC': 'PERCENTIEL.INC', 'PERCENTILE.EXC': 'PERCENTIEL.EXC', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..1bd8231f71 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'TAN.LICZBY.ZESP', LARGE: 'MAX.K', SMALL: 'MIN.K', + 'RANK.EQ': 'POZYCJA.NAJW', + RANK: 'POZYCJA', PERCENTILE: 'PERCENTYL', 'PERCENTILE.INC': 'PERCENTYL.PRZEDZ.ZAMK', 'PERCENTILE.EXC': 'PERCENTYL.PRZEDZ.OTW', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..5272290d62 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'MAIOR', SMALL: 'MENOR', + 'RANK.EQ': 'ORDEM.EQ', + RANK: 'ORDEM', PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INC', 'PERCENTILE.EXC': 'PERCENTIL.EXC', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..d8d99a1284 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'МНИМ.TAN', LARGE: 'НАИБОЛЬШИЙ', SMALL: 'НАИМЕНЬШИЙ', + 'RANK.EQ': 'РАНГ.РВ', + RANK: 'РАНГ', PERCENTILE: 'ПЕРСЕНТИЛЬ', 'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ', 'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..0a5a3abc34 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'IMTAN', LARGE: 'STÖRSTA', SMALL: 'MINSTA', + 'RANK.EQ': 'RANG.EKV', + RANK: 'RANG', PERCENTILE: 'PERCENTIL', 'PERCENTILE.INC': 'PERCENTIL.INK', 'PERCENTILE.EXC': 'PERCENTIL.EXK', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..d3f3806b2a 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -387,6 +387,8 @@ const dictionary: RawTranslationPackage = { IMTAN: 'SANTAN', LARGE: 'BÜYÜK', SMALL: 'KÜÇÜK', + 'RANK.EQ': 'RANK.EQ', + RANK: 'RANK', PERCENTILE: 'YÜZDEBİRLİK', 'PERCENTILE.INC': 'YÜZDEBİRLİK.DHL', 'PERCENTILE.EXC': 'YÜZDEBİRLİK.HRC', diff --git a/src/interpreter/functionMetadata/categories/statistical.ts b/src/interpreter/functionMetadata/categories/statistical.ts index 7a536e4cd2..dde42f0115 100644 --- a/src/interpreter/functionMetadata/categories/statistical.ts +++ b/src/interpreter/functionMetadata/categories/statistical.ts @@ -444,6 +444,13 @@ export const STATISTICAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=QUARTILE.INC(A1:A10, 3)'], }, + 'RANK.EQ': { + category: 'Statistical', + shortDescription: 'Returns the rank of a number in a range; tied numbers share the top rank.', + parameters: [{name: 'number', description: 'The number whose rank to find.'}, {name: 'ref', description: 'The range of numbers to rank against; non-numeric cells are ignored.'}, {name: 'order', description: 'Optional. 0 or omitted ranks in descending order; any other value ranks in ascending order.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=RANK.EQ(A1, A1:A10)', '=RANK.EQ(A1, A1:A10, 1)'], + }, RSQ: { category: 'Statistical', shortDescription: 'Returns the squared correlation coefficient between two data sets.', diff --git a/src/interpreter/plugin/MedianPlugin.ts b/src/interpreter/plugin/MedianPlugin.ts index a6a2a1078e..2744e17cd6 100644 --- a/src/interpreter/plugin/MedianPlugin.ts +++ b/src/interpreter/plugin/MedianPlugin.ts @@ -38,6 +38,18 @@ export class MedianPlugin extends FunctionPlugin implements FunctionPluginTypech {argumentType: FunctionArgumentType.NUMBER, minValue: 1}, ], }, + 'RANK.EQ': { + method: 'rankEq', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER}, + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER, defaultValue: 0}, + ], + }, + } + + public static aliases = { + RANK: 'RANK.EQ', } /** @@ -100,4 +112,32 @@ export class MedianPlugin extends FunctionPlugin implements FunctionPluginTypech } ) } + + /** + * Corresponds to RANK.EQ(number, ref, [order]) and RANK(number, ref, [order]). + * + * Returns the rank of a number within a range; tied numbers share the top rank of the group. + * Order 0 (default) ranks descending, any other value ranks ascending. Only numeric cells of `ref` are considered. + * + * @param ast - procedure AST node + * @param state - interpreter state + */ + public rankEq(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('RANK.EQ'), + (target: number, range: SimpleRangeValue, order: number) => { + const values = range.valuesFromTopLeftCorner() + const numbers = values.filter((value): value is number => typeof value === 'number') + const errorInRange = values.find((value): value is CellError => value instanceof CellError) + if (errorInRange !== undefined) { + return errorInRange + } + if (!numbers.includes(target)) { + return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) + } + const isAscending = order !== 0 + const better = numbers.filter(value => isAscending ? value < target : value > target) + return better.length + 1 + } + ) + } } From 764c0924219416764b1eef48befe63b57f527a8b Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 09:55:24 +0000 Subject: [PATCH 2/5] 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 778260a19e..6b7d2fd162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,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 new functions: RANK.EQ and its alias RANK. +- Added new functions: RANK.EQ and its alias RANK. [#1786](https://github.com/handsontable/hyperformula/pull/1786) ### Changed From 3ec0eeef69775b554374d9f8c61bb8bf9880eeed Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 5 Oct 2026 11:19:57 +0000 Subject: [PATCH 3/5] Fix RANK.EQ ignoring typed numeric cells (percent, date, currency) Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/MedianPlugin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/interpreter/plugin/MedianPlugin.ts b/src/interpreter/plugin/MedianPlugin.ts index 2744e17cd6..3825e46374 100644 --- a/src/interpreter/plugin/MedianPlugin.ts +++ b/src/interpreter/plugin/MedianPlugin.ts @@ -125,11 +125,9 @@ export class MedianPlugin extends FunctionPlugin implements FunctionPluginTypech public rankEq(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('RANK.EQ'), (target: number, range: SimpleRangeValue, order: number) => { - const values = range.valuesFromTopLeftCorner() - const numbers = values.filter((value): value is number => typeof value === 'number') - const errorInRange = values.find((value): value is CellError => value instanceof CellError) - if (errorInRange !== undefined) { - return errorInRange + const numbers = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()) + if (numbers instanceof CellError) { + return numbers } if (!numbers.includes(target)) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) From bb2b2b7d88b70c2f493033a8573017e0fd55b090 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 8 Oct 2026 03:53:14 +0000 Subject: [PATCH 4/5] Register RANK and RANK.EQ 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..7352d00434 100644 --- a/src/license/functionCapabilities.ts +++ b/src/license/functionCapabilities.ts @@ -92,7 +92,7 @@ const UNGROUPED_FUNCTIONS = [ 'ISREF', 'LCM', 'LOG10', 'LOGNORM.DIST', 'LOGNORM.INV', 'MAXA', 'MAXPOOL', 'MEDIANPOOL', 'MINA', 'MIRR', 'MMULT', 'MULTINOMIAL', 'NEGBINOM.DIST', 'NETWORKDAYS.INTL', 'NOMINAL', 'NORM.DIST', 'NORM.INV', 'NORM.S.DIST', 'NORM.S.INV', 'NPER', 'OCT2BIN', 'OCT2DEC', 'OCT2HEX', 'PDURATION', 'PERCENTILE.EXC', - 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', + 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'RANK', 'RANK.EQ', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', 'SERIESSUM', 'SHEET', 'SHEETS', 'SINH', 'SKEW', 'SKEW.P', 'SLOPE', 'SPLIT', 'SQRTPI', 'STANDARDIZE', 'STEYX', 'SUMX2MY2', 'SUMX2PY2', 'SYD', 'T.DIST', 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST', 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'UNICODE', 'VARA', 'VARPA', From ea4aab3905575e65e7a6dceafc68f4b4a270ec34 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 12 Oct 2026 03:46:10 +0000 Subject: [PATCH 5/5] docs: place the changelog entry under Unreleased Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7d2fd162..6b2679d40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added new functions: RANK.EQ and its alias RANK. [#1786](https://github.com/handsontable/hyperformula/pull/1786) + ## [3.5.0] - 2026-10-09 ### 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 new functions: RANK.EQ and its alias RANK. [#1786](https://github.com/handsontable/hyperformula/pull/1786) ### Changed