-
Notifications
You must be signed in to change notification settings - Fork 168
Feature/choosecols function #1734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
86892f1
8e0ff87
9b2901b
f915ac4
8a03fb4
0df4d1f
77a01d9
f9d182e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,17 +3,58 @@ | |
| * Copyright (c) 2025 Handsoncode. All rights reserved. | ||
| */ | ||
|
|
||
| import {AbsoluteCellRange} from '../../AbsoluteCellRange' | ||
| import {ArraySize} from '../../ArraySize' | ||
| import {CellError, ErrorType} from '../../Cell' | ||
| import {ErrorMessage} from '../../error-message' | ||
| import {AstNodeType, ProcedureAst} from '../../parser' | ||
| import {Ast, AstNodeType, ProcedureAst} from '../../parser' | ||
| import {coerceScalarToBoolean} from '../ArithmeticHelper' | ||
| import {InterpreterState} from '../InterpreterState' | ||
| import {InternalScalarValue, InterpreterValue} from '../InterpreterValue' | ||
| import {getRawValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue' | ||
| import {SimpleRangeValue} from '../../SimpleRangeValue' | ||
| import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' | ||
|
|
||
| /** A CHOOSECOLS index classified without evaluating a formula expression. */ | ||
| type ChooseColsLiteralIndex = | ||
| | {kind: 'value', value: number} | ||
| | {kind: 'invalid'} | ||
| | {kind: 'unresolved'} | ||
|
|
||
| export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck<ArrayPlugin> { | ||
| /** | ||
| * Classifies an index literal for static CHOOSECOLS result-size prediction. | ||
| * | ||
| * @param {Ast} argument - The column-index argument to inspect without evaluating formulas. | ||
| * @returns {ChooseColsLiteralIndex} A coerced literal value, an invalid marker, or an unresolved marker. | ||
| */ | ||
| private parseChooseColsLiteralIndex(argument: Ast): ChooseColsLiteralIndex { | ||
| if (argument.type === AstNodeType.NUMBER) { | ||
| return {kind: 'value', value: Math.trunc(argument.value)} | ||
| } | ||
|
|
||
| if (argument.type === AstNodeType.STRING) { | ||
| const coercedValue = this.arithmeticHelper.coerceToMaybeNumber(argument.value) | ||
| if (coercedValue === undefined) { | ||
| return {kind: 'invalid'} | ||
| } | ||
| return {kind: 'value', value: Math.trunc(getRawValue(coercedValue))} | ||
| } | ||
|
|
||
| if (argument.type === AstNodeType.PLUS_UNARY_OP && argument.value.type === AstNodeType.NUMBER) { | ||
| return {kind: 'value', value: Math.trunc(argument.value.value)} | ||
| } | ||
|
|
||
| if (argument.type === AstNodeType.MINUS_UNARY_OP && argument.value.type === AstNodeType.NUMBER) { | ||
| return {kind: 'value', value: Math.trunc(-argument.value.value)} | ||
| } | ||
|
|
||
| if (argument.type === AstNodeType.PARENTHESIS) { | ||
| return this.parseChooseColsLiteralIndex(argument.expression) | ||
| } | ||
|
|
||
| return {kind: 'unresolved'} | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| public static implementedFunctions: ImplementedFunctions = { | ||
| 'ARRAYFORMULA': { | ||
| method: 'arrayformula', | ||
|
|
@@ -43,6 +84,17 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche | |
| ], | ||
| repeatLastArgs: 1, | ||
| }, | ||
| 'CHOOSECOLS': { | ||
| method: 'choosecols', | ||
| sizeOfResultArrayMethod: 'choosecolsArraySize', | ||
| enableArrayArithmeticForArguments: true, | ||
| parameters: [ | ||
| {argumentType: FunctionArgumentType.RANGE}, | ||
| {argumentType: FunctionArgumentType.NUMBER}, | ||
| ], | ||
| repeatLastArgs: 1, | ||
| vectorizationForbidden: true, | ||
| }, | ||
| 'VSTACK': { | ||
| method: 'vstack', | ||
| sizeOfResultArrayMethod: 'vstackArraySize', | ||
|
|
@@ -166,6 +218,103 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche | |
| return new ArraySize(width, height) | ||
| } | ||
|
|
||
| /** | ||
| * Corresponds to CHOOSECOLS(array, col_num1, [col_num2], ...). | ||
| * | ||
| * Returns the requested source columns in argument order. Positive indexes | ||
| * count from the left, negative indexes count from the right, and duplicate | ||
| * indexes duplicate their columns in the result. | ||
| * | ||
| * @param {ProcedureAst} ast - The parsed function-call AST node. | ||
| * @param {InterpreterState} state - The current interpreter evaluation state. | ||
| */ | ||
| public choosecols(ast: ProcedureAst, state: InterpreterState): InterpreterValue { | ||
| return this.runFunction(ast.args, state, this.metadata('CHOOSECOLS'), | ||
| (range: SimpleRangeValue, ...columnNumbers: number[]) => { | ||
| const sourceWidth = range.width() | ||
| const sourceHeight = range.height() | ||
|
|
||
| if (sourceHeight === 0 || sourceWidth === 0) { | ||
| return new CellError(ErrorType.NA, ErrorMessage.EmptyRange) | ||
| } | ||
|
|
||
| const columnIndexes = columnNumbers.map(columnNumber => Math.trunc(columnNumber)) | ||
|
|
||
| if (columnIndexes.some(columnIndex => | ||
| !Number.isFinite(columnIndex) || columnIndex === 0 || Math.abs(columnIndex) > sourceWidth | ||
| )) { | ||
| return new CellError(ErrorType.VALUE, ErrorMessage.IndexBounds) | ||
| } | ||
|
|
||
| const zeroBasedColumnIndexes = columnIndexes.map(columnIndex => | ||
| columnIndex > 0 ? columnIndex - 1 : sourceWidth + columnIndex | ||
| ) | ||
|
|
||
| const sourceRange = range.range | ||
| if (sourceRange !== undefined) { | ||
| const selectedColumns = zeroBasedColumnIndexes.map(columnIndex => { | ||
| const columnRange = AbsoluteCellRange.spanFrom( | ||
| sourceRange.getAddress(columnIndex, 0), | ||
| 1, | ||
| sourceHeight, | ||
| ) | ||
| return SimpleRangeValue.onlyRange(columnRange, this.dependencyGraph).data | ||
| }) | ||
| const result = Array.from({length: sourceHeight}, (_, row) => | ||
| selectedColumns.map(column => column[row][0]) | ||
| ) | ||
| return SimpleRangeValue.onlyValues(result) | ||
| } | ||
|
|
||
| const result = range.data.map(row => | ||
| zeroBasedColumnIndexes.map(columnIndex => row[columnIndex]) | ||
| ) | ||
| return SimpleRangeValue.onlyValues(result) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Predicts the CHOOSECOLS spill size from the source height and index count. | ||
| * | ||
| * Invalid literals and unbounded result heights are rejected before spill | ||
| * allocation so a neighboring cell cannot mask a statically known error. | ||
| * | ||
| * @param {ProcedureAst} ast - The parsed function-call AST node. | ||
| * @param {InterpreterState} state - The current interpreter evaluation state. | ||
| */ | ||
| public choosecolsArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same finding as on #1722's TAKE:
|
||
| if (ast.args.length < 2) { | ||
| return ArraySize.error() | ||
| } | ||
|
|
||
| const metadata = this.metadata('CHOOSECOLS') | ||
| const sourceSize = this.arraySizeForAst( | ||
| ast.args[0], | ||
| new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)), | ||
| ) | ||
|
|
||
| if (!Number.isFinite(sourceSize.height) || sourceSize.height < 1) { | ||
| return ArraySize.error() | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| for (const argument of ast.args.slice(1)) { | ||
| const index = this.parseChooseColsLiteralIndex(argument) | ||
| if ( | ||
| index.kind === 'invalid' | ||
| || (index.kind === 'value' && ( | ||
| !Number.isFinite(index.value) | ||
| || index.value === 0 | ||
| || Math.abs(index.value) > sourceSize.width | ||
| )) | ||
| ) { | ||
| return ArraySize.error() | ||
| } | ||
| } | ||
|
|
||
| return new ArraySize(ast.args.length - 1, sourceSize.height) | ||
| } | ||
|
|
||
| /** | ||
| * Corresponds to VSTACK(array1, [array2], ...) | ||
| * | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.