Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Added a message after `npm i hyperformula` that points AI assistants to the SKILLS file. The message needs `--foreground-scripts` to be visible in npm, and pnpm 10 needs the `postinstall` script to be approved first. [#1829](https://github.com/handsontable/hyperformula/pull/1829)
- Added the `PERCENTRANK.INC` (with its `PERCENTRANK` alias), `MODE.MULT`, `FREQUENCY` and `PROB` statistical functions. [#1818](https://github.com/handsontable/hyperformula/pull/1818)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions docs/guide/known-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ a circular reference.
* The INDEX function doesn't support returning whole rows or columns of the source range – it always returns the contents of a single cell.
* The FILTER function accepts either single rows of equal width or single columns of equal height. In other words, all arrays passed to the FILTER function must have equal dimensions, and at least one of those dimensions must be 1.
* Array-producing functions (e.g., SEQUENCE, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation.
* `MODE.MULT` and `FREQUENCY` reserve their result area before evaluation, from the size of their arguments: `MODE.MULT` half as many rows as values passed to it, rounded down, and at least one, `FREQUENCY` one row per cell of `bins_array` plus one. Rows the result does not fill stay empty, and a value in any reserved cell makes the formula return `#SPILL!`.
* The TEXT function does not accept embedded double-quote literals in the format string. In Excel, `""` inside a format string is an escape sequence for a literal `"` character — e.g. `=TEXT(1234.5, "#,##0.00 ""zł""")` returns `"1,234.50 zł"`. If your application requires this escape sequence, supply a custom [`stringifyCurrency`](currency-handling.md) callback.

### UNIQUE function
Expand Down
1 change: 1 addition & 0 deletions hyperformula/src/error-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class ErrorMessage {
public static OutOfSheet = 'Resulting reference is out of the sheet.'
public static WrongType = 'Wrong type of argument.'
public static NaN = 'NaN or infinite value encountered.'
public static ProbabilityBounds = 'Probabilities need to be greater than 0, at most 1, and sum up to 1.'
public static EqualLength = 'Ranges need to be of equal length.'
public static Negative = 'Value cannot be negative.'
public static NotBinary = 'String does not represent a binary number.'
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTIL',
'PERCENTILE.INC': 'PERCENTIL.INC',
'PERCENTILE.EXC': 'PERCENTIL.EXC',
PERCENTRANK: 'PERCENTRANK',
'PERCENTRANK.INC': 'PERCENTRANK.INC',
'MODE.MULT': 'MODE.MULT',
FREQUENCY: 'ČETNOSTI',
PROB: 'PROB',
QUARTILE: 'QUARTIL',
'QUARTILE.INC': 'QUARTIL.INC',
'QUARTILE.EXC': 'QUARTIL.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'FRAKTIL',
'PERCENTILE.INC': 'FRAKTIL.MEDTAG',
'PERCENTILE.EXC': 'FRAKTIL.UDELAD',
PERCENTRANK: 'PROCENTPLADS',
'PERCENTRANK.INC': 'PROCENTPLADS.MEDTAG',
'MODE.MULT': 'HYPPIGST.FLERE',
FREQUENCY: 'FREKVENS',
PROB: 'SANDSYNLIGHED',
QUARTILE: 'KVARTIL',
'QUARTILE.INC': 'KVARTIL.MEDTAG',
'QUARTILE.EXC': 'KVARTIL.UDELAD',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'QUANTIL',
'PERCENTILE.INC': 'QUANTIL.INKL',
'PERCENTILE.EXC': 'QUANTIL.EXKL',
PERCENTRANK: 'QUANTILSRANG',
'PERCENTRANK.INC': 'QUANTILSRANG.INKL',
'MODE.MULT': 'MODUS.VIELF',
FREQUENCY: 'HÄUFIGKEIT',
PROB: 'WAHRSCHBEREICH',
QUARTILE: 'QUARTILE',
'QUARTILE.INC': 'QUARTILE.INKL',
'QUARTILE.EXC': 'QUARTILE.EXKL',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTILE',
'PERCENTILE.INC': 'PERCENTILE.INC',
'PERCENTILE.EXC': 'PERCENTILE.EXC',
PERCENTRANK: 'PERCENTRANK',
'PERCENTRANK.INC': 'PERCENTRANK.INC',
'MODE.MULT': 'MODE.MULT',
FREQUENCY: 'FREQUENCY',
PROB: 'PROB',
QUARTILE: 'QUARTILE',
'QUARTILE.INC': 'QUARTILE.INC',
'QUARTILE.EXC': 'QUARTILE.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ export const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTIL',
'PERCENTILE.INC': 'PERCENTIL.INC',
'PERCENTILE.EXC': 'PERCENTIL.EXC',
PERCENTRANK: 'RANGO.PERCENTIL',
'PERCENTRANK.INC': 'RANGO.PERCENTIL.INC',
'MODE.MULT': 'MODA.VARIOS',
FREQUENCY: 'FRECUENCIA',
PROB: 'PROBABILIDAD',
QUARTILE: 'CUARTIL',
'QUARTILE.INC': 'CUARTIL.INC',
'QUARTILE.EXC': 'CUARTIL.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PROSENTTIPISTE',
'PERCENTILE.INC': 'PROSENTTIPISTE.SIS',
'PERCENTILE.EXC': 'PROSENTTIPISTE.ULK',
PERCENTRANK: 'PROSENTTIJÄRJESTYS',
'PERCENTRANK.INC': 'PROSENTTIJÄRJESTYS.SIS',
'MODE.MULT': 'MOODI.USEA',
FREQUENCY: 'TAAJUUS',
PROB: 'TODENNÄKÖISYYS',
QUARTILE: 'NELJÄNNES',
'QUARTILE.INC': 'NELJÄNNES.SIS',
'QUARTILE.EXC': 'NELJÄNNES.ULK',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'CENTILE',
'PERCENTILE.INC': 'CENTILE.INCLURE',
'PERCENTILE.EXC': 'CENTILE.EXCLURE',
PERCENTRANK: 'RANG.POURCENTAGE',
'PERCENTRANK.INC': 'RANG.POURCENTAGE.INCLURE',
'MODE.MULT': 'MODE.MULTIPLE',
FREQUENCY: 'FREQUENCE',
PROB: 'PROBABILITE',
QUARTILE: 'QUARTILE',
'QUARTILE.INC': 'QUARTILE.INCLURE',
'QUARTILE.EXC': 'QUARTILE.EXCLURE',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTILIS',
'PERCENTILE.INC': 'PERCENTILIS.TARTALMAZ',
'PERCENTILE.EXC': 'PERCENTILIS.KIZÁR',
PERCENTRANK: 'SZÁZALÉKRANG',
'PERCENTRANK.INC': 'SZÁZALÉKRANG.TARTALMAZ',
'MODE.MULT': 'MÓDUSZ.TÖBB',
FREQUENCY: 'GYAKORISÁG',
PROB: 'VALÓSZÍNŰSÉG',
QUARTILE: 'KVARTILIS',
'QUARTILE.INC': 'KVARTILIS.TARTALMAZ',
'QUARTILE.EXC': 'KVARTILIS.KIZÁR',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/idID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTILE',
'PERCENTILE.INC': 'PERCENTILE.INC',
'PERCENTILE.EXC': 'PERCENTILE.EXC',
PERCENTRANK: 'PERCENTRANK',
'PERCENTRANK.INC': 'PERCENTRANK.INC',
'MODE.MULT': 'MODE.MULT',
FREQUENCY: 'FREQUENCY',
PROB: 'PROB',
QUARTILE: 'QUARTILE',
'QUARTILE.INC': 'QUARTILE.INC',
'QUARTILE.EXC': 'QUARTILE.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTILE',
'PERCENTILE.INC': 'INC.PERCENTILE',
'PERCENTILE.EXC': 'ESC.PERCENTILE',
PERCENTRANK: 'PERCENT.RANGO',
'PERCENTRANK.INC': 'INC.PERCENT.RANGO',
'MODE.MULT': 'MODA.MULT',
FREQUENCY: 'FREQUENZA',
PROB: 'PROBABILITÀ',
QUARTILE: 'QUARTILE',
'QUARTILE.INC': 'INC.QUARTILE',
'QUARTILE.EXC': 'ESC.QUARTILE',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERSENTIL',
'PERCENTILE.INC': 'PERSENTIL.INK',
'PERCENTILE.EXC': 'PERSENTIL.EKS',
PERCENTRANK: 'PROSENTDEL',
'PERCENTRANK.INC': 'PROSENTDEL.INK',
'MODE.MULT': 'MODUS.MULT',
FREQUENCY: 'FREKVENS',
PROB: 'SANNSYNLIG',
QUARTILE: 'KVARTIL',
'QUARTILE.INC': 'KVARTIL.INK',
'QUARTILE.EXC': 'KVARTIL.EKS',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTIEL',
'PERCENTILE.INC': 'PERCENTIEL.INC',
'PERCENTILE.EXC': 'PERCENTIEL.EXC',
PERCENTRANK: 'PERCENT.RANG',
'PERCENTRANK.INC': 'PROCENTRANG.INC',
'MODE.MULT': 'MODUS.MEERV',
FREQUENCY: 'FREQUENCY',
PROB: 'KANS',
QUARTILE: 'KWARTIEL',
'QUARTILE.INC': 'KWARTIEL.INC',
'QUARTILE.EXC': 'KWARTIEL.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTYL',
'PERCENTILE.INC': 'PERCENTYL.PRZEDZ.ZAMK',
'PERCENTILE.EXC': 'PERCENTYL.PRZEDZ.OTW',
PERCENTRANK: 'PROCENT.POZYCJA',
'PERCENTRANK.INC': 'PROC.POZ.PRZEDZ.ZAMK',
'MODE.MULT': 'WYST.NAJCZĘŚCIEJ.TABL',
FREQUENCY: 'CZĘSTOŚĆ',
PROB: 'PRAWDPD',
QUARTILE: 'KWARTYL',
'QUARTILE.INC': 'KWARTYL.PRZEDZ.ZAMK',
'QUARTILE.EXC': 'KWARTYL.PRZEDZ.OTW',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTIL',
'PERCENTILE.INC': 'PERCENTIL.INC',
'PERCENTILE.EXC': 'PERCENTIL.EXC',
PERCENTRANK: 'ORDEM.PERCENTUAL',
'PERCENTRANK.INC': 'ORDEM.PERCENTUAL.INC',
'MODE.MULT': 'MODO.MÚLT',
FREQUENCY: 'FREQUÊNCIA',
PROB: 'PROB',
QUARTILE: 'QUARTIL',
'QUARTILE.INC': 'QUARTIL.INC',
'QUARTILE.EXC': 'QUARTIL.EXC',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'ПЕРСЕНТИЛЬ',
'PERCENTILE.INC': 'ПРОЦЕНТИЛЬ.ВКЛ',
'PERCENTILE.EXC': 'ПРОЦЕНТИЛЬ.ИСКЛ',
PERCENTRANK: 'ПРОЦЕНТРАНГ',
'PERCENTRANK.INC': 'ПРОЦЕНТРАНГ.ВКЛ',
'MODE.MULT': 'МОДА.НСК',
FREQUENCY: 'ЧАСТОТА',
PROB: 'ВЕРОЯТНОСТЬ',
QUARTILE: 'КВАРТИЛЬ',
'QUARTILE.INC': 'КВАРТИЛЬ.ВКЛ',
'QUARTILE.EXC': 'КВАРТИЛЬ.ИСКЛ',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'PERCENTIL',
'PERCENTILE.INC': 'PERCENTIL.INK',
'PERCENTILE.EXC': 'PERCENTIL.EXK',
PERCENTRANK: 'PROCENTRANG',
'PERCENTRANK.INC': 'PROCENTRANG.INK',
'MODE.MULT': 'TYPVÄRDE.FLERA',
FREQUENCY: 'FREKVENS',
PROB: 'SANNOLIKHET',
QUARTILE: 'KVARTIL',
'QUARTILE.INC': 'KVARTIL.INK',
'QUARTILE.EXC': 'KVARTIL.EXK',
Expand Down
5 changes: 5 additions & 0 deletions hyperformula/src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ const dictionary: RawTranslationPackage = {
PERCENTILE: 'YÜZDEBİRLİK',
'PERCENTILE.INC': 'YÜZDEBİRLİK.DHL',
'PERCENTILE.EXC': 'YÜZDEBİRLİK.HRC',
PERCENTRANK: 'YÜZDERANK',
'PERCENTRANK.INC': 'YÜZDERANK.DHL',
'MODE.MULT': 'ENÇOK_OLAN.ÇOK',
FREQUENCY: 'SIKLIK',
PROB: 'OLASILIK',
QUARTILE: 'DÖRTTEBİRLİK',
'QUARTILE.INC': 'DÖRTTEBİRLİK.DHL',
'QUARTILE.EXC': 'DÖRTTEBİRLİK.HRC',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ export const STATISTICAL_DOCS: Record<string, FunctionDoc> = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=FISHERINV(0.5)'],
},
FREQUENCY: {
category: 'Statistical',
shortDescription: 'Returns a column that counts how many numbers of data_array fall into each interval set by bins_array.',
parameters: [{name: 'data_array', description: 'The range or array of values to count; text, booleans and empty cells are skipped.'}, {name: 'bins_array', description: 'The range or array of interval upper bounds; the result has one row more than there are bounds, the last row counting the values above the largest bound.'}],
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=FREQUENCY(A1:A10, B1:B3)'],
},
GAMMA: {
category: 'Statistical',
shortDescription: 'Returns value of Gamma function.',
Expand Down Expand Up @@ -346,6 +353,13 @@ export const STATISTICAL_DOCS: Record<string, FunctionDoc> = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=MEDIAN(1, 2, 3)', '=MEDIAN(A1:A10)'],
},
'MODE.MULT': {
category: 'Statistical',
shortDescription: 'Returns a column of the most frequently occurring numbers, in order of first occurrence; #N/A when no number repeats.',
parameters: [{name: 'number1', description: 'A number, cell reference, or range to search for the most frequent numbers; text, booleans and empty cells are skipped. Further numbers or ranges can be passed as additional arguments.'}],
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=MODE.MULT(1, 1, 2, 2, 3)', '=MODE.MULT(A1:A10)'],
},
MIN: {
category: 'Statistical',
shortDescription: 'Returns the minimum value in a list of arguments.',
Expand Down Expand Up @@ -416,6 +430,13 @@ export const STATISTICAL_DOCS: Record<string, FunctionDoc> = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=PERCENTILE.INC(A1:A10, 0.9)'],
},
'PERCENTRANK.INC': {
category: 'Statistical',
shortDescription: 'Returns the rank of a value within a range as a fraction between 0 and 1, inclusive of both ends.',
parameters: [{name: 'array', description: 'The range of values that defines the ranking; text, booleans and empty cells are skipped.'}, {name: 'x', description: 'The value to rank; #N/A when it lies outside of the numbers in array.'}, {name: 'significance', description: 'The number of decimal digits of the result, truncated rather than rounded; at least 1, 3 by default.'}],
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=PERCENTRANK.INC(A1:A10, 5)', '=PERCENTRANK.INC(A1:A10, 5, 2)'],
},
PHI: {
category: 'Statistical',
shortDescription: 'Returns probability density of normal distribution.',
Expand All @@ -430,6 +451,13 @@ export const STATISTICAL_DOCS: Record<string, FunctionDoc> = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=POISSON.DIST(3, 5, FALSE())', '=POISSON.DIST(3, 5, TRUE())'],
},
PROB: {
category: 'Statistical',
shortDescription: 'Returns the probability that a value falls within the given limits, from a range of values and their probabilities.',
parameters: [{name: 'x_range', description: 'The range of numeric values.'}, {name: 'prob_range', description: 'The range of probabilities of the values in x_range, of the same size; each must be greater than 0 and at most 1, and together they must sum up to 1.'}, {name: 'lower_limit', description: 'The lower limit of the values, or the single value whose probability is returned when upper_limit is omitted.'}, {name: 'upper_limit', description: 'The upper limit of the values; the limits are inclusive.'}],
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=PROB(A1:A4, B1:B4, 2)', '=PROB(A1:A4, B1:B4, 2, 3)'],
},
'QUARTILE.EXC': {
category: 'Statistical',
shortDescription: 'Returns the quartile of a data set, based on exclusive percentile values.',
Expand Down
80 changes: 79 additions & 1 deletion hyperformula/src/interpreter/plugin/PercentilePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,49 @@ function percentileExclusive(sortedVals: number[], k: number): number | CellErro
}

/**
* Interpreter plugin for percentile and quartile statistical functions.
* Computes the inclusive percent rank of `x` in the sorted values, the inverse of {@link percentileInclusive}:
* the rank is `index / (n - 1)`, where `index` is the position of the first element equal to `x`, or the
* linearly interpolated position when `x` falls between two elements.
* Assumes sortedVals is non-empty and x lies within [first, last].
*
* @param sortedVals - pre-sorted array of numeric values (ascending)
* @param x - the value to rank
* @returns the rank in [0, 1], before truncation to the requested significance
*/
function percentRankInclusive(sortedVals: number[], x: number): number {
const n = sortedVals.length
if (n === 1) {
return 1
}
const upperIndex = sortedVals.findIndex(value => value >= x)
if (sortedVals[upperIndex] === x) {
return upperIndex / (n - 1)
}
const lowerIndex = upperIndex - 1
const fraction = (x - sortedVals[lowerIndex]) / (sortedVals[upperIndex] - sortedVals[lowerIndex])
return (lowerIndex + fraction) / (n - 1)
}

const TRUNCATION_TOLERANCE = 1e-9

/** Beyond this many digits a double holds nothing more to keep, and `10 ** digits` would overflow for huge ones. */
const MAX_SIGNIFICANT_DIGITS = 15

/**
* Truncates (does not round) `value` to the given number of decimal digits. The tiny tolerance keeps
* results such as 0.3, which the division leaves a hair below, from being cut to 0.2999.
*
* @param value - a non-negative number
* @param digits - the number of decimal digits to keep
* @returns the truncated value
*/
function truncateToDigits(value: number, digits: number): number {
const scale = Math.pow(10, Math.min(digits, MAX_SIGNIFICANT_DIGITS))
return Math.floor(value * scale + TRUNCATION_TOLERANCE) / scale
}

/**
* Interpreter plugin for percentile, percent rank and quartile statistical functions.
*
* Implements inclusive (INC) and exclusive (EXC) interpolation variants.
* QUARTILE functions delegate to PERCENTILE by converting quart index to
Expand All @@ -81,6 +123,14 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy
{argumentType: FunctionArgumentType.NUMBER, greaterThan: 0, lessThan: 1},
],
},
'PERCENTRANK.INC': {
method: 'percentrank',
parameters: [
{argumentType: FunctionArgumentType.RANGE},
{argumentType: FunctionArgumentType.NUMBER},
{argumentType: FunctionArgumentType.NUMBER, minValue: 1, defaultValue: 3},
],
},
'QUARTILE.INC': {
method: 'quartile',
parameters: [
Expand All @@ -99,6 +149,7 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy

public static aliases = {
PERCENTILE: 'PERCENTILE.INC',
PERCENTRANK: 'PERCENTRANK.INC',
QUARTILE: 'QUARTILE.INC',
}

Expand Down Expand Up @@ -144,6 +195,33 @@ export class PercentilePlugin extends FunctionPlugin implements FunctionPluginTy
)
}

/**
* Corresponds to PERCENTRANK(array, x, [significance]) and PERCENTRANK.INC(array, x, [significance]).
*
* Returns the rank of `x` within the numbers of `array` as a fraction in [0, 1], truncated (not rounded)
* to `significance` digits (3 by default; a fractional value is truncated to an integer).
* Returns #N/A when `array` holds no numbers or `x` lies outside of them, #NUM! when `significance` is below 1.
*
* @param ast - procedure AST node
* @param state - interpreter state
* @returns the truncated percent rank, or CellError on invalid input
*/
public percentrank(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
return this.runFunction(ast.args, state, this.metadata('PERCENTRANK.INC'),
(range: SimpleRangeValue, x: number, significance: number) => {
const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner())
if (vals instanceof CellError) {
return vals
}
vals.sort((a, b) => a - b)
if (vals.length === 0 || x < vals[0] || x > vals[vals.length - 1]) {
return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty PERCENTRANK.INC returns wrong error

Low Severity

PERCENTRANK.INC returns #N/A when array contains no numbers, because the empty case is grouped with “x is outside the data”. Excel returns #NUM! for an empty array and #N/A only when x lies outside a non-empty set. getSortedValues already surfaces #NUM! for that empty case.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2d8f790. Configure here.

}
return truncateToDigits(percentRankInclusive(vals, x), Math.trunc(significance))
}
)
}

/**
* Corresponds to QUARTILE(array, quart) and QUARTILE.INC(array, quart).
*
Expand Down
Loading
Loading