Skip to content
Merged
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 @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Added new functions: VSTACK, HSTACK. [#1698](https://github.com/handsontable/hyperformula/pull/1698)
- Added a new function: `XIRR`. [#1701](https://github.com/handsontable/hyperformula/pull/1701)
- Added the SORT function. [#1707](https://github.com/handsontable/hyperformula/pull/1707)
- Added an Indonesian (Bahasa Indonesia) language pack. [#1674](https://github.com/handsontable/hyperformula/pull/1674)
- Added a `stringifyCurrency` config option that lets you plug in a custom currency formatter for the `TEXT` function. [#1145](https://github.com/handsontable/hyperformula/issues/1145)

Expand Down
1 change: 1 addition & 0 deletions docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Total number of functions: **{{ $page.functionsCount }}**
| SEQUENCE | Returns an array of sequential numbers. | SEQUENCE(Rows, [Cols], [Start], [Step]) |
| VSTACK | Stacks arrays vertically into a single array. | VSTACK(Array1, [Array2], ...[ArrayN]) |
| HSTACK | Stacks arrays horizontally into a single array. | HSTACK(Array1, [Array2], ...[ArrayN]) |
| SORT | Sorts the rows or columns of an array. | SORT(Array, [SortIndex], [SortOrder], [ByCol]) |

### Date and time

Expand Down
8 changes: 8 additions & 0 deletions docs/guide/known-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ a circular reference.
* 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.
* 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.

### SORT function

* The `SortIndex` argument accepts a single key only. Multi-key sorting through an array constant (for example `=SORT(A1:B9, {1,2})`) is not supported; sort by one column or row at a time.

* The `SortOrder` argument must be exactly `1` (ascending) or `-1` (descending). Any other value returns a `#VALUE!` error.

* Ordering (including mixed types, empty cells, and text collation) follows HyperFormula's own comparison rules, which honor the `caseSensitive` and `accentSensitive` configuration options. Numbers sort before text, and text before logical values.

### OFFSET function

HyperFormula resolves the OFFSET function at parse time rather than during evaluation. The parser inspects the arguments and rewrites the expression into a plain cell reference or range. This keeps the dependency graph accurate but imposes several restrictions.
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'ODPIS.LIN',
SORT: 'SORT',
SPLIT: 'SPLIT',
SQRT: 'ODMOCNINA',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'LA',
SORT: 'SORTER',
SPLIT: 'SPLIT',
SQRT: 'KVROD',
STDEVA: 'STDAFVV',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINHYP',
SLN: 'LIA',
SORT: 'SORTIEREN',
SPLIT: 'SPLIT',
SQRT: 'WURZEL',
STDEVA: 'STABWA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'SLN',
SORT: 'SORT',
SPLIT: 'SPLIT',
SQRT: 'SQRT',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const dictionary: RawTranslationPackage = {
SIN: 'SENO',
SINH: 'SENOH',
SLN: 'SLN',
SORT: 'ORDENAR',
SPLIT: 'SPLIT',
SQRT: 'RAIZ',
STDEVA: 'DESVESTA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'STP',
SORT: 'LAJITTELE',
SPLIT: 'SPLIT',
SQRT: 'NELIÖJUURI',
STDEVA: 'KESKIHAJONTAA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'AMORLIN',
SORT: 'TRIER',
SPLIT: 'SPLIT',
SQRT: 'RACINE',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'LCSA',
SORT: 'SORBA.RENDEZ',
SPLIT: 'SPLIT',
SQRT: 'GYÖK',
STDEVA: 'SZÓRÁSA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/idID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'GSL',
SORT: 'SORT',
SPLIT: 'PISAH',
SQRT: 'AKAR',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SEN',
SINH: 'SENH',
SLN: 'AMMORT.COST',
SORT: 'DATI.ORDINA',
SPLIT: 'SPLIT',
SQRT: 'RADQ',
STDEVA: 'DEV.ST.VALORI',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'LINAVS',
SORT: 'SORTER',
SPLIT: 'SPLIT',
SQRT: 'ROT',
STDEVA: 'STDAVVIKA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'LIN.AFSCHR',
SORT: 'SORTEREN',
SPLIT: 'SPLIT',
SQRT: 'WORTEL',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'SLN',
SORT: 'SORTUJ',
SPLIT: 'PODZIEL.TEKST',
SQRT: 'PIERWIASTEK',
STDEVA: 'ODCH.STANDARDOWE.A',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SEN',
SINH: 'SENH',
SLN: 'DPD',
SORT: 'ORDENAR',
SPLIT: 'SPLIT',
SQRT: 'RAIZ',
STDEVA: 'DESVPADA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'АПЛ',
SORT: 'СОРТ',
SPLIT: 'SPLIT',
SQRT: 'КОРЕНЬ',
STDEVA: 'СТАНДОТКЛОНА',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SIN',
SINH: 'SINH',
SLN: 'LINAVSKR',
SORT: 'SORTERA',
SPLIT: 'SPLIT',
SQRT: 'ROT',
STDEVA: 'STDEVA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const dictionary: RawTranslationPackage = {
SIN: 'SİN',
SINH: 'SINH',
SLN: 'DA',
SORT: 'SIRALA',
SPLIT: 'SPLIT',
SQRT: 'KAREKÖK',
STDEVA: 'STDSAPMAA',
Expand Down
162 changes: 162 additions & 0 deletions src/interpreter/plugin/SortPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import {ArraySize} from '../../ArraySize'
import {CellError, ErrorType} from '../../Cell'
import {ErrorMessage} from '../../error-message'
import {ProcedureAst} from '../../parser'
import {InterpreterState} from '../InterpreterState'
import {EmptyValue, InternalNoErrorScalarValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue'
import {SimpleRangeValue} from '../../SimpleRangeValue'
import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin'

/**
* Plugin implementing the SORT spreadsheet function.
*
* SORT(array, [sort_index], [sort_order], [by_col]) returns the elements of
* `array` sorted along one dimension. The returned array has the same shape as
* the input. By default it reorders rows using the first column as the key,
* ascending. Ordering is delegated to {@link ArithmeticHelper} so that mixed
* types, empty cells, and locale collation behave exactly as elsewhere in the
* engine.
*/
export class SortPlugin extends FunctionPlugin implements FunctionPluginTypecheck<SortPlugin> {
public static implementedFunctions: ImplementedFunctions = {
'SORT': {
method: 'sort',
sizeOfResultArrayMethod: 'sortArraySize',
enableArrayArithmeticForArguments: true,
parameters: [
{argumentType: FunctionArgumentType.RANGE},
{argumentType: FunctionArgumentType.NUMBER, defaultValue: 1, emptyAsDefault: true},
{argumentType: FunctionArgumentType.NUMBER, defaultValue: 1, emptyAsDefault: true},
{argumentType: FunctionArgumentType.BOOLEAN, defaultValue: false, emptyAsDefault: true},
],
vectorizationForbidden: true,
},
Comment thread
cursor[bot] marked this conversation as resolved.
}

/**
* Corresponds to SORT(array, [sort_index], [sort_order], [by_col]).
*
* `sort_order` is validated strictly to {1, -1} (the documented contract);
* any other value yields #VALUE!. `sort_index` is a 1-based index into the sort
* dimension (columns when sorting rows, rows when `by_col` is TRUE). Errors found
* anywhere in the input range are propagated.
*
* @param {ProcedureAst} ast - the parsed function-call AST node.
* @param {InterpreterState} state - current interpreter evaluation state.
*/
public sort(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
return this.runFunction(ast.args, state, this.metadata('SORT'),
(range: SimpleRangeValue, sortIndex: number, sortOrder: number, byCol: boolean) => {
if (sortOrder !== 1 && sortOrder !== -1) {
return new CellError(ErrorType.VALUE, ErrorMessage.BadMode)
}

const data = range.data
const height = range.height()
const width = range.width()

const firstError = SortPlugin.findFirstError(data)
if (firstError !== undefined) {
return firstError
}

// An empty input range (e.g. a whole-column reference to an empty sheet)
// yields no rows/columns to sort. Return #N/A rather than letting the empty
// 2-D array reach SimpleRangeValue.onlyValues, which reads data[0].length and
// would throw. Mirrors UNIQUE/FILTER's empty-range handling.
if (data.length === 0 || data[0].length === 0) {
return new CellError(ErrorType.NA, ErrorMessage.EmptyRange)
}

const index = Math.trunc(sortIndex)
const sortDimension = byCol ? height : width
if (index < 1) {
return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne)
}
if (index > sortDimension) {
return new CellError(ErrorType.VALUE, ErrorMessage.ValueLarge)
}

const keyIndex = index - 1
const compare = (a: InternalNoErrorScalarValue, b: InternalNoErrorScalarValue): number => {
// Excel keeps empty cells at the end of the result regardless of
// sort_order, rather than coercing them to 0 and ordering by value.
// Force empties last (direction-independent) before delegating the
// rest to ArithmeticHelper.
const aEmpty = a === EmptyValue
const bEmpty = b === EmptyValue
if (aEmpty || bEmpty) {
if (aEmpty && bEmpty) {
return 0
}
return aEmpty ? 1 : -1
}
if (this.arithmeticHelper.lt(a, b)) {
return -sortOrder
}
if (this.arithmeticHelper.gt(a, b)) {
return sortOrder
}
return 0
}

if (byCol) {
// Reorder columns; the key of column c is data[keyIndex][c].
const order = Array.from({length: width}, (_, c) => c)
order.sort((c1, c2) => compare(
data[keyIndex][c1] as InternalNoErrorScalarValue,
data[keyIndex][c2] as InternalNoErrorScalarValue,
))
const result: InternalScalarValue[][] = data.map(row => order.map(c => row[c]))
return SimpleRangeValue.onlyValues(result)
}

// Reorder rows; the key of row r is data[r][keyIndex].
const rows: InternalScalarValue[][] = data.map(row => row.slice())
rows.sort((r1, r2) => compare(
r1[keyIndex] as InternalNoErrorScalarValue,
r2[keyIndex] as InternalNoErrorScalarValue,
))
return SimpleRangeValue.onlyValues(rows)
Comment thread
marcin-kordas-hoc marked this conversation as resolved.
}
)
}

/**
* Predicts the output array size for SORT at parse time.
* The result is always the same shape as the input array.
*
* @param {ProcedureAst} ast - the parsed function-call AST node.
* @param {InterpreterState} state - current interpreter evaluation state.
*/
public sortArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize {
if (ast.args.length < 1 || ast.args.length > 4) {
return ArraySize.error()
}
const metadata = this.metadata('SORT')
const subChecks = ast.args.map((arg) =>
this.arraySizeForAst(arg, new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false))))
// Return a fresh ArraySize (isRef defaults to false). Propagating the input's
// ArraySize verbatim would carry its `isRef` flag, and ArraySize.isScalar()
// treats a ref as scalar — which would collapse the spilled result into a
// single cell. SORT's output always matches the input shape.
return new ArraySize(subChecks[0].width, subChecks[0].height)
Comment thread
marcin-kordas-hoc marked this conversation as resolved.
}

/** Returns the first {@link CellError} found in a 2-D array, or undefined. */
private static findFirstError(data: InternalScalarValue[][]): CellError | undefined {
for (const row of data) {
for (const cell of row) {
if (cell instanceof CellError) {
return cell
}
}
}
return undefined
}
}
1 change: 1 addition & 0 deletions src/interpreter/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export {RadiansPlugin} from './RadiansPlugin'
export {RadixConversionPlugin} from './RadixConversionPlugin'
export {RandomPlugin} from './RandomPlugin'
export {SequencePlugin} from './SequencePlugin'
export {SortPlugin} from './SortPlugin'
export {RoundingPlugin} from './RoundingPlugin'
export {SqrtPlugin} from './SqrtPlugin'
export {ConditionalAggregationPlugin} from './ConditionalAggregationPlugin'
Expand Down
Loading