From 838efd199d92ecff71ae39b6191a4a111edcfca8 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 9 Apr 2026 17:51:56 +0000 Subject: [PATCH 01/27] Docs: add basic usage snippets to framework integration guides Add idiomatic code examples to React, Angular, Vue, and Svelte integration pages showing HyperFormula initialization and reading calculated values. Each guide uses framework-specific patterns (React hooks, Angular service, Vue ref/markRaw, Svelte reactivity). Closes HF-122. --- docs/guide/integration-with-angular.md | 64 ++++++++++++++++++++++++++ docs/guide/integration-with-react.md | 56 ++++++++++++++++++++++ docs/guide/integration-with-svelte.md | 46 ++++++++++++++++++ docs/guide/integration-with-vue.md | 42 +++++++++++++++++ 4 files changed, 208 insertions(+) diff --git a/docs/guide/integration-with-angular.md b/docs/guide/integration-with-angular.md index 8f78e2097..1e6b57998 100644 --- a/docs/guide/integration-with-angular.md +++ b/docs/guide/integration-with-angular.md @@ -4,6 +4,70 @@ Installing HyperFormula in an Angular application works the same as with vanilla For more details, see the [client-side installation](client-side-installation.md) section. +## Basic usage + +### Step 1. Create a service + +Wrap HyperFormula in an Angular service. Create the instance in the constructor and expose methods for reading data. + +```typescript +import { Injectable, OnDestroy } from '@angular/core'; +import { HyperFormula } from 'hyperformula'; + +@Injectable({ providedIn: 'root' }) +export class SpreadsheetService implements OnDestroy { + private hf: HyperFormula; + private sheetId = 0; + + constructor() { + // Create a HyperFormula instance with initial data. + this.hf = HyperFormula.buildFromArray( + [ + [10, 20, '=SUM(A1:B1)'], + [30, 40, '=SUM(A2:B2)'], + ], + { licenseKey: 'gpl-v3' } + ); + } + + /** Return calculated values for the entire sheet. */ + getSheetValues(): (number | string | null)[][] { + return this.hf.getSheetValues(this.sheetId); + } + + ngOnDestroy(): void { + this.hf.destroy(); + } +} +``` + +### Step 2. Use the service in a component + +Inject the service and display the calculated values. + +```typescript +import { Component } from '@angular/core'; +import { SpreadsheetService } from './spreadsheet.service'; + +@Component({ + selector: 'app-spreadsheet', + template: ` + + + + +
{{ cell }}
+ `, +}) +export class SpreadsheetComponent { + data: (number | string | null)[][]; + + constructor(private spreadsheet: SpreadsheetService) { + this.data = this.spreadsheet.getSheetValues(); + } +} +``` + ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/angular-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-react.md b/docs/guide/integration-with-react.md index d4bc7fe75..1591aec85 100644 --- a/docs/guide/integration-with-react.md +++ b/docs/guide/integration-with-react.md @@ -4,6 +4,62 @@ Installing HyperFormula in a React application works the same as with vanilla Ja For more details, see the [client-side installation](client-side-installation.md) section. +## Basic usage + +### Step 1. Initialize HyperFormula + +Use `useRef` to hold the HyperFormula instance so it persists across re-renders. Initialize it inside a `useEffect` hook. + +```javascript +import { useRef, useEffect, useState } from 'react'; +import { HyperFormula } from 'hyperformula'; + +function SpreadsheetComponent() { + const hfRef = useRef(null); + const [sheetData, setSheetData] = useState([]); + + useEffect(() => { + // Create a HyperFormula instance with initial data. + hfRef.current = HyperFormula.buildFromArray( + [ + [10, 20, '=SUM(A1:B1)'], + [30, 40, '=SUM(A2:B2)'], + ], + { licenseKey: 'gpl-v3' } + ); + + // Read calculated values from the sheet. + const sheetId = 0; + setSheetData(hfRef.current.getSheetValues(sheetId)); + + return () => { + // Clean up the instance when the component unmounts. + hfRef.current?.destroy(); + }; + }, []); +``` + +### Step 2. Render the results + +Display the calculated values in a table. + +```javascript + return ( + + + {sheetData.map((row, rowIdx) => ( + + {row.map((cell, colIdx) => ( + + ))} + + ))} + +
{cell}
+ ); +} +``` + ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/react-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-svelte.md b/docs/guide/integration-with-svelte.md index 8b3a5f4b6..bd8aff85d 100644 --- a/docs/guide/integration-with-svelte.md +++ b/docs/guide/integration-with-svelte.md @@ -4,6 +4,52 @@ Installing HyperFormula in a Svelte application works the same as with vanilla J For more details, see the [client-side installation](client-side-installation.md) section. +## Basic usage + +### Step 1. Initialize HyperFormula + +Create the HyperFormula instance directly in the component's ` +``` + +### Step 2. Render the results + +Display the data in a table and trigger calculation with a button. + +```html + + + + {#each data as row, rowIdx} + + {#each row as cell, colIdx} + + {/each} + + {/each} +
{cell}
+``` + ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-vue.md b/docs/guide/integration-with-vue.md index eaa104e0e..68afa5a8e 100644 --- a/docs/guide/integration-with-vue.md +++ b/docs/guide/integration-with-vue.md @@ -4,6 +4,48 @@ Installing HyperFormula in a Vue application works the same as with vanilla Java For more details, see the [client-side installation](client-side-installation.md) section. +## Basic usage + +### Step 1. Initialize HyperFormula + +Wrap the HyperFormula instance in `markRaw` to prevent Vue's reactivity system from converting it into a proxy (see [Troubleshooting](#vue-reactivity-issues) below). Use `ref` for the data you want to display. + +```javascript + +``` + +### Step 2. Render the results + +Display the calculated values in a template. + +```html + +``` + ## Troubleshooting ### Vue reactivity issues From 3afa99b361ef597911ae6148ca4f320da081a60b Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 09:06:51 +0000 Subject: [PATCH 02/27] Implement DCOUNT database function Add new DatabasePlugin with DCOUNT(database, field, criteria) that counts numeric values in a specified field column for rows matching criteria. Supports field resolution by name (case-insensitive) and 1-based index, OR logic across criteria rows, AND logic within rows, and comparison operators in criteria values. Includes translations for all 17 languages. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + src/interpreter/plugin/DatabasePlugin.ts | 208 +++++++++++++++++++++++ src/interpreter/plugin/index.ts | 1 + 19 files changed, 226 insertions(+) create mode 100644 src/interpreter/plugin/DatabasePlugin.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 21858277a..5b2627823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Added `maxPendingLazyTransformations` configuration option to control memory usage by limiting accumulated transformations before cleanup. [#1629](https://github.com/handsontable/hyperformula/issues/1629) - Added a new function: TEXTJOIN. [#1640](https://github.com/handsontable/hyperformula/pull/1640) - Added a new function: SEQUENCE. [#1645](https://github.com/handsontable/hyperformula/pull/1645) +- Added a new function: DCOUNT. (#1623) ### Fixed diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 463bb1d2a..529055d4a 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'ROK360', DAYS: 'DAYS', DB: 'ODPIS.ZRYCH', + DCOUNT: 'DCOUNT', DDB: 'ODPIS.ZRYCH2', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 6525c442a..a8defd4dc 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGE360', DAYS: 'DAGE', DB: 'DB', + DCOUNT: 'ANTAL.DB', DDB: 'DSA', DEC2BIN: 'DEC.TIL.BIN', DEC2HEX: 'DEC.TIL.HEX', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index d99d6cc1c..817839f32 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'TAGE360', DAYS: 'TAGE', DB: 'GDA2', + DCOUNT: 'DBANZAHL', DDB: 'GDA', DEC2BIN: 'DEZINBIN', DEC2HEX: 'DEZINHEX', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 840e14a90..9111b3faf 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -76,6 +76,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAYS360', DAYS: 'DAYS', DB: 'DB', + DCOUNT: 'DCOUNT', DDB: 'DDB', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 1d8278ca5..a47b2836b 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -75,6 +75,7 @@ export const dictionary: RawTranslationPackage = { DAYS360: 'DIAS360', DAYS: 'DÍAS', DB: 'DB', + DCOUNT: 'BDCONTAR', DDB: 'DDB', DEC2BIN: 'DEC.A.BIN', DEC2HEX: 'DEC.A.HEX', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index cd11bc7ec..f1c52cd17 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'PÄIVÄT360', DAYS: 'PV', DB: 'DB', + DCOUNT: 'TLASKE', DDB: 'DDB', DEC2BIN: 'DESBIN', DEC2HEX: 'DESHEKSA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index ae1dd4389..811cc555e 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'JOURS360', DAYS: 'JOURS', DB: 'DB', + DCOUNT: 'BDNB', DDB: 'DDB', DEC2BIN: 'DECBIN', DEC2HEX: 'DECHEX', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index 223f12811..ade163820 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAYS360', DAYS: 'NAPOK', DB: 'DB', + DCOUNT: 'AB.DARAB', DDB: 'KCSA', DEC2BIN: 'DEC.BIN', DEC2HEX: 'DEC.HEX', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1ebbaa255..343099775 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'GIORNO360', DAYS: 'GIORNI', DB: 'AMMORT.FISSO', + DCOUNT: 'DB.CONTA.NUMERI', DDB: 'AMMORT', DEC2BIN: 'DECIMALE.BINARIO', DEC2HEX: 'DECIMALE.HEX', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 9ec608493..d926d0358 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGER360', DAYS: 'DAGER', DB: 'DAVSKR', + DCOUNT: 'ANTALLDB', DDB: 'DEGRAVS', DEC2BIN: 'DESTILBIN', DEC2HEX: 'DESTILHEKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 201318236..ef668df66 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGEN360', DAYS: 'DAGEN', DB: 'DB', + DCOUNT: 'DBAANTAL', DDB: 'DDB', DEC2BIN: 'DEC.N.BIN', DEC2HEX: 'DEC.N.HEX', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index acce07114..93d719b45 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DNI.360', DAYS: 'DNI', DB: 'DB', + DCOUNT: 'BD.ILE.REKORDÓW', DDB: 'DDB', DEC2BIN: 'DZIES.NA.DWÓJK', DEC2HEX: 'DZIES.NA.SZESN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index a161a5497..219d20fa7 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DIAS360', DAYS: 'DIAS', DB: 'BD', + DCOUNT: 'BDCONTAR', DDB: 'BDD', DEC2BIN: 'DECABIN', DEC2HEX: 'DECAHEX', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 8d07da729..aeb0c2327 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'ДНЕЙ360', DAYS: 'ДНИ', DB: 'ФУО', + DCOUNT: 'БСЧЁТ', DDB: 'ДДОБ', DEC2BIN: 'ДЕС.В.ДВ', DEC2HEX: 'ДЕС.В.ШЕСТН', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index c92269c33..a0b2a7596 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGAR360', DAYS: 'DAYS', DB: 'DB', + DCOUNT: 'DANTAL', DDB: 'DEGAVSKR', DEC2BIN: 'DEC.TILL.BIN', DEC2HEX: 'DEC.TILL.HEX', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 417b96bb9..d47c3143a 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -75,6 +75,7 @@ const dictionary: RawTranslationPackage = { DAYS360: 'GÜN360', DAYS: 'GÜNSAY', DB: 'AZALANBAKİYE', + DCOUNT: 'VSAY', DDB: 'ÇİFTAZALANBAKİYE', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts new file mode 100644 index 000000000..9bf1089ca --- /dev/null +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -0,0 +1,208 @@ +/** + * @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 {EmptyValue, getRawValue, InternalScalarValue, InterpreterValue, isExtendedNumber, RawScalarValue} from '../InterpreterValue' +import {SimpleRangeValue} from '../../SimpleRangeValue' +import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' +import {CriterionLambda} from '../Criterion' + +/** + * Parsed criterion for a single cell in the criteria range. + * Maps a database column index to a matching lambda. + */ +interface DatabaseCriterionEntry { + /** 0-based column index within the database range. */ + columnIndex: number, + /** Lambda that tests whether a raw cell value satisfies the criterion. */ + lambda: CriterionLambda, +} + +/** + * A single criteria row is a list of AND-ed criterion entries. + * Multiple criteria rows are OR-ed together. + */ +type DatabaseCriteriaRow = DatabaseCriterionEntry[] + +/** + * Interpreter plugin implementing Excel database functions. + * + * Currently implements: DCOUNT. + * Designed to be extended with DSUM, DAVERAGE, DMAX, DMIN, etc. + */ +export class DatabasePlugin extends FunctionPlugin implements FunctionPluginTypecheck { + + public static implementedFunctions: ImplementedFunctions = { + 'DCOUNT': { + method: 'dcount', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + } + + /** + * Counts cells containing numbers in the specified field of a database range, + * for rows that match all criteria. + * + * DCOUNT(database, field, criteria) + */ + public dcount(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DCOUNT'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + count++ + } + } + } + + return count + }) + } + + /** + * Resolves the field argument to a 0-based column index within the database range. + * + * @param database - The database range (first row = headers). + * @param field - A string (header name, case-insensitive) or number (1-based column index). + * @returns 0-based column index, or CellError if field is invalid. + */ + private resolveFieldIndex(database: SimpleRangeValue, field: RawScalarValue): number | CellError { + const headers = database.data[0] + + if (typeof field === 'string') { + const lowerField = field.toLowerCase() + for (let i = 0; i < headers.length; i++) { + const header = headers[i] + if (typeof header === 'string' && header.toLowerCase() === lowerField) { + return i + } + } + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + + if (isExtendedNumber(field)) { + const index = Math.trunc(getRawValue(field)) + if (index < 1 || index > headers.length) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + return index - 1 + } + + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + + /** + * Parses the criteria range into an array of criteria rows. + * Each row is a list of AND-ed conditions. Rows are OR-ed together. + * + * @param database - The database range (first row = headers). + * @param criteria - The criteria range (first row = header labels, subsequent rows = conditions). + * @returns Array of criteria rows, or CellError if a criterion cannot be parsed. + */ + private buildDatabaseCriteria(database: SimpleRangeValue, criteria: SimpleRangeValue): DatabaseCriteriaRow[] | CellError { + const dbHeaders = database.data[0] + const criteriaData = criteria.data + const criteriaHeaders = criteriaData[0] + + // Map each criteria column to a database column index (or -1 if no match) + const criteriaColumnMapping: number[] = criteriaHeaders.map(criteriaHeader => { + if (typeof criteriaHeader !== 'string') { + return -1 + } + const lowerHeader = criteriaHeader.toLowerCase() + return dbHeaders.findIndex( + dbHeader => typeof dbHeader === 'string' && dbHeader.toLowerCase() === lowerHeader + ) + }) + + const rows: DatabaseCriteriaRow[] = [] + + for (let rowIdx = 1; rowIdx < criteriaData.length; rowIdx++) { + const row: DatabaseCriteriaRow = [] + + for (let colIdx = 0; colIdx < criteriaHeaders.length; colIdx++) { + const dbColIndex = criteriaColumnMapping[colIdx] + if (dbColIndex === -1) { + continue // Unknown criteria header — ignore + } + + const criterionValue = criteriaData[rowIdx]?.[colIdx] + + // Empty/blank criteria cell = match-all for that column — skip + if (criterionValue === EmptyValue || criterionValue === undefined || criterionValue === null) { + continue + } + + const rawCriterionValue = isExtendedNumber(criterionValue) ? getRawValue(criterionValue) : criterionValue + + const criterionPackage = this.interpreter.criterionBuilder.fromCellValue( + rawCriterionValue as RawScalarValue, + this.arithmeticHelper + ) + + if (criterionPackage === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.BadCriterion) + } + + row.push({ + columnIndex: dbColIndex, + lambda: criterionPackage.lambda, + }) + } + + rows.push(row) + } + + return rows + } + + /** + * Tests whether a database data row matches any of the criteria rows (OR logic). + * Within each criteria row, all conditions must match (AND logic). + * + * @param dataRow - A single row of data from the database (excluding the header row). + * @param criteriaRows - Parsed criteria rows. + * @returns true if the row qualifies, false otherwise. + */ + private rowMatchesCriteria(dataRow: InternalScalarValue[], criteriaRows: DatabaseCriteriaRow[]): boolean { + if (criteriaRows.length === 0) { + return false + } + + return criteriaRows.some(criteriaRow => { + if (criteriaRow.length === 0) { + return true // Empty criteria row = match all + } + + return criteriaRow.every(entry => { + const cellValue = dataRow[entry.columnIndex] + const rawValue = isExtendedNumber(cellValue) ? getRawValue(cellValue) : cellValue + return entry.lambda(rawValue) + }) + }) + } +} diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts index 2b1191c63..e86ba4f2a 100644 --- a/src/interpreter/plugin/index.ts +++ b/src/interpreter/plugin/index.ts @@ -13,6 +13,7 @@ export {CharPlugin} from './CharPlugin' export {CodePlugin} from './CodePlugin' export {CountBlankPlugin} from './CountBlankPlugin' export {CountUniquePlugin} from './CountUniquePlugin' +export {DatabasePlugin} from './DatabasePlugin' export {DateTimePlugin} from './DateTimePlugin' export {DegreesPlugin} from './DegreesPlugin' export {DeltaPlugin} from './DeltaPlugin' From 6884afe2c1ee0784ab79e0411dfc6c019842c47a Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 10:04:46 +0000 Subject: [PATCH 03/27] ci: retrigger From f29ad0043668867674784a17437ae94dc93b7913 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 10:12:17 +0000 Subject: [PATCH 04/27] ci: retrigger after tests PR created From 007a62f25581023e1b4dde3dd8521dd5ba05c32c Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 14:52:41 +0000 Subject: [PATCH 05/27] Implement DSUM, DCOUNTA, DPRODUCT database functions Add three new database functions to DatabasePlugin with i18n translations for all 17 languages. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/csCZ.ts | 7 + src/i18n/languages/daDK.ts | 7 + src/i18n/languages/deDE.ts | 7 + src/i18n/languages/enGB.ts | 7 + src/i18n/languages/esES.ts | 7 + src/i18n/languages/fiFI.ts | 7 + src/i18n/languages/frFR.ts | 7 + src/i18n/languages/huHU.ts | 7 + src/i18n/languages/itIT.ts | 7 + src/i18n/languages/nbNO.ts | 7 + src/i18n/languages/nlNL.ts | 7 + src/i18n/languages/plPL.ts | 7 + src/i18n/languages/ptPT.ts | 7 + src/i18n/languages/ruRU.ts | 7 + src/i18n/languages/svSE.ts | 7 + src/i18n/languages/trTR.ts | 7 + src/interpreter/plugin/DatabasePlugin.ts | 334 ++++++++++++++++++++++- 17 files changed, 444 insertions(+), 2 deletions(-) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 529055d4a..813264f5c 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'ROK360', DAYS: 'DAYS', DB: 'ODPIS.ZRYCH', + DAVERAGE: 'DPRUMER', DCOUNT: 'DCOUNT', + DCOUNTA: 'DPOCET2', + DGET: 'DZISKAT', + DMAX: 'DMAX', + DMIN: 'DMIN', + DPRODUCT: 'DSOUCIN', + DSUM: 'DSOUCET', DDB: 'ODPIS.ZRYCH2', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index a8defd4dc..0f3c4fc20 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGE360', DAYS: 'DAGE', DB: 'DB', + DAVERAGE: 'DGENNEMSNIT', DCOUNT: 'ANTAL.DB', + DCOUNTA: 'ANTAL.DBV', + DGET: 'DHENT', + DMAX: 'DMAKS', + DMIN: 'DMIN', + DPRODUCT: 'DPRODUKT', + DSUM: 'DSUM', DDB: 'DSA', DEC2BIN: 'DEC.TIL.BIN', DEC2HEX: 'DEC.TIL.HEX', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 817839f32..d84a0ce54 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'TAGE360', DAYS: 'TAGE', DB: 'GDA2', + DAVERAGE: 'DBMITTELWERT', DCOUNT: 'DBANZAHL', + DCOUNTA: 'DBANZAHL2', + DGET: 'DBAUSZUG', + DMAX: 'DBMAX', + DMIN: 'DBMIN', + DPRODUCT: 'DBPRODUKT', + DSUM: 'DBSUMME', DDB: 'GDA', DEC2BIN: 'DEZINBIN', DEC2HEX: 'DEZINHEX', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 9111b3faf..02add2614 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -76,7 +76,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAYS360', DAYS: 'DAYS', DB: 'DB', + DAVERAGE: 'DAVERAGE', DCOUNT: 'DCOUNT', + DCOUNTA: 'DCOUNTA', + DGET: 'DGET', + DMAX: 'DMAX', + DMIN: 'DMIN', + DPRODUCT: 'DPRODUCT', + DSUM: 'DSUM', DDB: 'DDB', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index a47b2836b..083ae1ffc 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -75,7 +75,14 @@ export const dictionary: RawTranslationPackage = { DAYS360: 'DIAS360', DAYS: 'DÍAS', DB: 'DB', + DAVERAGE: 'BDPROMEDIO', DCOUNT: 'BDCONTAR', + DCOUNTA: 'BDCONTARA', + DGET: 'BDEXTRAER', + DMAX: 'BDMAX', + DMIN: 'BDMIN', + DPRODUCT: 'BDPRODUCTO', + DSUM: 'BDSUMA', DDB: 'DDB', DEC2BIN: 'DEC.A.BIN', DEC2HEX: 'DEC.A.HEX', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index f1c52cd17..1d6ede25d 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'PÄIVÄT360', DAYS: 'PV', DB: 'DB', + DAVERAGE: 'TKESKIARVO', DCOUNT: 'TLASKE', + DCOUNTA: 'TLASKE.A', + DGET: 'TNOUDA', + DMAX: 'TMAKS', + DMIN: 'TMIN', + DPRODUCT: 'TTULO', + DSUM: 'TSUMMA', DDB: 'DDB', DEC2BIN: 'DESBIN', DEC2HEX: 'DESHEKSA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 811cc555e..b34baa198 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'JOURS360', DAYS: 'JOURS', DB: 'DB', + DAVERAGE: 'BDMOYENNE', DCOUNT: 'BDNB', + DCOUNTA: 'BDNBVAL', + DGET: 'BDLIRE', + DMAX: 'BDMAX', + DMIN: 'BDMIN', + DPRODUCT: 'BDPRODUIT', + DSUM: 'BDSOMME', DDB: 'DDB', DEC2BIN: 'DECBIN', DEC2HEX: 'DECHEX', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index ade163820..e5c23e86e 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAYS360', DAYS: 'NAPOK', DB: 'DB', + DAVERAGE: 'AB.ÁTLAG', DCOUNT: 'AB.DARAB', + DCOUNTA: 'AB.DARAB2', + DGET: 'AB.MEZŐ', + DMAX: 'AB.MAX', + DMIN: 'AB.MIN', + DPRODUCT: 'AB.SZORZAT', + DSUM: 'AB.SZUM', DDB: 'KCSA', DEC2BIN: 'DEC.BIN', DEC2HEX: 'DEC.HEX', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 343099775..3b1697e1a 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'GIORNO360', DAYS: 'GIORNI', DB: 'AMMORT.FISSO', + DAVERAGE: 'DB.MEDIA', DCOUNT: 'DB.CONTA.NUMERI', + DCOUNTA: 'DB.CONTA.VALORI', + DGET: 'DB.VALORI', + DMAX: 'DB.MAX', + DMIN: 'DB.MIN', + DPRODUCT: 'DB.PRODOTTO', + DSUM: 'DB.SOMMA', DDB: 'AMMORT', DEC2BIN: 'DECIMALE.BINARIO', DEC2HEX: 'DECIMALE.HEX', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index d926d0358..fc9c33658 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGER360', DAYS: 'DAGER', DB: 'DAVSKR', + DAVERAGE: 'DGJENNOMSNITT', DCOUNT: 'ANTALLDB', + DCOUNTA: 'DANTALLA', + DGET: 'DHENT', + DMAX: 'DMAKS', + DMIN: 'DMIN', + DPRODUCT: 'DPRODUKT', + DSUM: 'DSUMMER', DDB: 'DEGRAVS', DEC2BIN: 'DESTILBIN', DEC2HEX: 'DESTILHEKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index ef668df66..de219a953 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGEN360', DAYS: 'DAGEN', DB: 'DB', + DAVERAGE: 'DBGEMIDDELDE', DCOUNT: 'DBAANTAL', + DCOUNTA: 'DBAAANTALC', + DGET: 'DBLEZEN', + DMAX: 'DBMAX', + DMIN: 'DBMIN', + DPRODUCT: 'DBPRODUKT', + DSUM: 'DBSOM', DDB: 'DDB', DEC2BIN: 'DEC.N.BIN', DEC2HEX: 'DEC.N.HEX', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 93d719b45..200d6694c 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DNI.360', DAYS: 'DNI', DB: 'DB', + DAVERAGE: 'BD.SREDNIA', DCOUNT: 'BD.ILE.REKORDÓW', + DCOUNTA: 'BD.ILE.REKORDOW.A', + DGET: 'BD.POLE', + DMAX: 'BD.MAX', + DMIN: 'BD.MIN', + DPRODUCT: 'BD.ILOCZYN', + DSUM: 'BD.SUMA', DDB: 'DDB', DEC2BIN: 'DZIES.NA.DWÓJK', DEC2HEX: 'DZIES.NA.SZESN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 219d20fa7..4cc581e90 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DIAS360', DAYS: 'DIAS', DB: 'BD', + DAVERAGE: 'BDMÉDIA', DCOUNT: 'BDCONTAR', + DCOUNTA: 'BDCONTARA', + DGET: 'BDEXTRAIR', + DMAX: 'BDMÁX', + DMIN: 'BDMÍN', + DPRODUCT: 'BDPRODUTO', + DSUM: 'BDSOMA', DDB: 'BDD', DEC2BIN: 'DECABIN', DEC2HEX: 'DECAHEX', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index aeb0c2327..8a3d68877 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'ДНЕЙ360', DAYS: 'ДНИ', DB: 'ФУО', + DAVERAGE: 'БДСРЕДНЕЕ', DCOUNT: 'БСЧЁТ', + DCOUNTA: 'БСЧЕТА', + DGET: 'БИЗВЛЕЧЬ', + DMAX: 'БДМАКС', + DMIN: 'БДМИН', + DPRODUCT: 'БДПРОИЗВЕД', + DSUM: 'БДСУММ', DDB: 'ДДОБ', DEC2BIN: 'ДЕС.В.ДВ', DEC2HEX: 'ДЕС.В.ШЕСТН', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index a0b2a7596..551bd31c5 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGAR360', DAYS: 'DAYS', DB: 'DB', + DAVERAGE: 'DMEDEL', DCOUNT: 'DANTAL', + DCOUNTA: 'DANTALV', + DGET: 'DHÄMTA', + DMAX: 'DMAX', + DMIN: 'DMIN', + DPRODUCT: 'DPRODUKT', + DSUM: 'DSUMMA', DDB: 'DEGAVSKR', DEC2BIN: 'DEC.TILL.BIN', DEC2HEX: 'DEC.TILL.HEX', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index d47c3143a..150341739 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -75,7 +75,14 @@ const dictionary: RawTranslationPackage = { DAYS360: 'GÜN360', DAYS: 'GÜNSAY', DB: 'AZALANBAKİYE', + DAVERAGE: 'VORTALAMA', DCOUNT: 'VSAY', + DCOUNTA: 'VSAYMA', + DGET: 'VAL', + DMAX: 'VMAKS', + DMIN: 'VMİN', + DPRODUCT: 'VÇARPIM', + DSUM: 'VTOPLA', DDB: 'ÇİFTAZALANBAKİYE', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 9bf1089ca..ba891631e 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -32,8 +32,7 @@ type DatabaseCriteriaRow = DatabaseCriterionEntry[] /** * Interpreter plugin implementing Excel database functions. * - * Currently implements: DCOUNT. - * Designed to be extended with DSUM, DAVERAGE, DMAX, DMIN, etc. + * Implements: DAVERAGE, DCOUNT, DCOUNTA, DGET, DMAX, DMIN, DPRODUCT, DSUM. */ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginTypecheck { @@ -46,6 +45,62 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType {argumentType: FunctionArgumentType.RANGE}, ], }, + 'DCOUNTA': { + method: 'dcounta', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DPRODUCT': { + method: 'dproduct', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DSUM': { + method: 'dsum', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DAVERAGE': { + method: 'daverage', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DGET': { + method: 'dget', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DMAX': { + method: 'dmax', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DMIN': { + method: 'dmin', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, } /** @@ -83,6 +138,281 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType }) } + /** + * Counts all non-blank cells in the specified field of a database range, + * for rows that match all criteria. + * + * DCOUNTA(database, field, criteria) + */ + public dcounta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DCOUNTA'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue !== EmptyValue && cellValue !== undefined && cellValue !== null) { + count++ + } + } + } + + return count + }) + } + + /** + * Returns the product of numeric values in the specified field of a database range, + * for rows that match all criteria. + * + * DPRODUCT(database, field, criteria) + */ + public dproduct(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DPRODUCT'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let product = 1 + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + product *= getRawValue(cellValue) as number + hasNumeric = true + } + } + } + + return hasNumeric ? product : 0 + }) + } + + /** + * Returns the sum of numeric values in the specified field of a database range, + * for rows that match all criteria. + * + * DSUM(database, field, criteria) + */ + public dsum(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DSUM'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let sum = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + sum += getRawValue(cellValue) as number + } + } + } + + return sum + }) + } + + /** + * Returns the average of numeric values in the specified field of a database range, + * for rows that match all criteria. + * Returns #DIV/0! when no numeric values are found. + * + * DAVERAGE(database, field, criteria) + */ + public daverage(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DAVERAGE'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let sum = 0 + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + sum += getRawValue(cellValue) as number + count++ + } + } + } + + if (count === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + + return sum / count + }) + } + + /** + * Returns a single value from the specified field of a database range, + * for the row that matches all criteria. + * Returns #VALUE! if no rows match, #NUM! if more than one row matches. + * + * DGET(database, field, criteria) + */ + public dget(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DGET'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let matchedValue: InternalScalarValue | undefined + let matchCount = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + matchCount++ + if (matchCount > 1) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + matchedValue = dbData[rowIdx][fieldIndex] + } + } + + if (matchCount === 0) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + + return matchedValue === EmptyValue || matchedValue === undefined || matchedValue === null + ? 0 + : matchedValue + }) + } + + /** + * Returns the maximum numeric value in the specified field of a database range, + * for rows that match all criteria. + * Returns 0 when no numeric values are found (Excel behavior). + * + * DMAX(database, field, criteria) + */ + public dmax(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DMAX'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let max = -Infinity + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + const numValue = getRawValue(cellValue) as number + if (numValue > max) { + max = numValue + } + hasNumeric = true + } + } + } + + return hasNumeric ? max : 0 + }) + } + + /** + * Returns the minimum numeric value in the specified field of a database range, + * for rows that match all criteria. + * Returns 0 when no numeric values are found (Excel behavior). + * + * DMIN(database, field, criteria) + */ + public dmin(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DMIN'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const dbData = database.data + let min = Infinity + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + const numValue = getRawValue(cellValue) as number + if (numValue < min) { + min = numValue + } + hasNumeric = true + } + } + } + + return hasNumeric ? min : 0 + }) + } + /** * Resolves the field argument to a 0-based column index within the database range. * From 2ea85faa3311cda13889da66e889c75f7221b704 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 15:01:02 +0000 Subject: [PATCH 06/27] fixup! Implement DAVERAGE, DMAX, DMIN, DGET database functions Remove unnecessary type assertions in DAVERAGE, DMAX, DMIN methods. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index ba891631e..3dda34ca5 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -273,7 +273,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - sum += getRawValue(cellValue) as number + sum += getRawValue(cellValue) count++ } } @@ -359,7 +359,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - const numValue = getRawValue(cellValue) as number + const numValue = getRawValue(cellValue) if (numValue > max) { max = numValue } @@ -400,7 +400,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - const numValue = getRawValue(cellValue) as number + const numValue = getRawValue(cellValue) if (numValue < min) { min = numValue } From 3ae09c8f9fb7be9235c5f7a5bf89465dc5ffdb94 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 15:11:33 +0000 Subject: [PATCH 07/27] Implement DSTDEV, DSTDEVP, DVAR, DVARP database functions Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/csCZ.ts | 4 + src/i18n/languages/daDK.ts | 4 + src/i18n/languages/deDE.ts | 4 + src/i18n/languages/enGB.ts | 4 + src/i18n/languages/esES.ts | 4 + src/i18n/languages/fiFI.ts | 4 + src/i18n/languages/frFR.ts | 4 + src/i18n/languages/huHU.ts | 4 + src/i18n/languages/itIT.ts | 4 + src/i18n/languages/nbNO.ts | 4 + src/i18n/languages/nlNL.ts | 4 + src/i18n/languages/plPL.ts | 4 + src/i18n/languages/ptPT.ts | 4 + src/i18n/languages/ruRU.ts | 4 + src/i18n/languages/svSE.ts | 4 + src/i18n/languages/trTR.ts | 4 + src/interpreter/plugin/DatabasePlugin.ts | 189 ++++++++++++++++++++++- 17 files changed, 252 insertions(+), 1 deletion(-) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 813264f5c..dd72b170e 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DMAX', DMIN: 'DMIN', DPRODUCT: 'DSOUCIN', + DSTDEV: 'DSMODCH.VYBER', + DSTDEVP: 'DSMODCH', DSUM: 'DSOUCET', + DVAR: 'DVAR.VYBER', + DVARP: 'DVAR', DDB: 'ODPIS.ZRYCH2', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 0f3c4fc20..fbe402417 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DMAKS', DMIN: 'DMIN', DPRODUCT: 'DPRODUKT', + DSTDEV: 'DSTDAFV', + DSTDEVP: 'DSTDAFVP', DSUM: 'DSUM', + DVAR: 'DVARIANS', + DVARP: 'DVARIANSP', DDB: 'DSA', DEC2BIN: 'DEC.TIL.BIN', DEC2HEX: 'DEC.TIL.HEX', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index d84a0ce54..24a8ec03b 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DBMAX', DMIN: 'DBMIN', DPRODUCT: 'DBPRODUKT', + DSTDEV: 'DBSTDABW', + DSTDEVP: 'DBSTDABWN', DSUM: 'DBSUMME', + DVAR: 'DBVARIANZ', + DVARP: 'DBVARIANZEN', DDB: 'GDA', DEC2BIN: 'DEZINBIN', DEC2HEX: 'DEZINHEX', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 02add2614..e878f897a 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -83,7 +83,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DMAX', DMIN: 'DMIN', DPRODUCT: 'DPRODUCT', + DSTDEV: 'DSTDEV', + DSTDEVP: 'DSTDEVP', DSUM: 'DSUM', + DVAR: 'DVAR', + DVARP: 'DVARP', DDB: 'DDB', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 083ae1ffc..d36d14fd0 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -82,7 +82,11 @@ export const dictionary: RawTranslationPackage = { DMAX: 'BDMAX', DMIN: 'BDMIN', DPRODUCT: 'BDPRODUCTO', + DSTDEV: 'BDDESVEST', + DSTDEVP: 'BDDESVESTP', DSUM: 'BDSUMA', + DVAR: 'BDVAR', + DVARP: 'BDVARP', DDB: 'DDB', DEC2BIN: 'DEC.A.BIN', DEC2HEX: 'DEC.A.HEX', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 1d6ede25d..5735278d8 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'TMAKS', DMIN: 'TMIN', DPRODUCT: 'TTULO', + DSTDEV: 'TKESKIHAJONTA', + DSTDEVP: 'TKESKIHAJONTAP', DSUM: 'TSUMMA', + DVAR: 'TVARIANSSI', + DVARP: 'TVARIANSSIP', DDB: 'DDB', DEC2BIN: 'DESBIN', DEC2HEX: 'DESHEKSA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index b34baa198..044c70715 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'BDMAX', DMIN: 'BDMIN', DPRODUCT: 'BDPRODUIT', + DSTDEV: 'BDECARTYPE', + DSTDEVP: 'BDECARTYPEP', DSUM: 'BDSOMME', + DVAR: 'BDVAR', + DVARP: 'BDVARP', DDB: 'DDB', DEC2BIN: 'DECBIN', DEC2HEX: 'DECHEX', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e5c23e86e..3a5119222 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'AB.MAX', DMIN: 'AB.MIN', DPRODUCT: 'AB.SZORZAT', + DSTDEV: 'AB.SZÓRÁS', + DSTDEVP: 'AB.SZÓRÁS2', DSUM: 'AB.SZUM', + DVAR: 'AB.VAR', + DVARP: 'AB.VAR2', DDB: 'KCSA', DEC2BIN: 'DEC.BIN', DEC2HEX: 'DEC.HEX', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 3b1697e1a..2716d3471 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DB.MAX', DMIN: 'DB.MIN', DPRODUCT: 'DB.PRODOTTO', + DSTDEV: 'DB.DEV.ST', + DSTDEVP: 'DB.DEV.ST.POP', DSUM: 'DB.SOMMA', + DVAR: 'DB.VAR', + DVARP: 'DB.VAR.POP', DDB: 'AMMORT', DEC2BIN: 'DECIMALE.BINARIO', DEC2HEX: 'DECIMALE.HEX', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index fc9c33658..495a36ccd 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DMAKS', DMIN: 'DMIN', DPRODUCT: 'DPRODUKT', + DSTDEV: 'DSTDAV', + DSTDEVP: 'DSTDAVP', DSUM: 'DSUMMER', + DVAR: 'DVARIANS', + DVARP: 'DVARIANSP', DDB: 'DEGRAVS', DEC2BIN: 'DESTILBIN', DEC2HEX: 'DESTILHEKS', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index de219a953..98efe1d15 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DBMAX', DMIN: 'DBMIN', DPRODUCT: 'DBPRODUKT', + DSTDEV: 'DBSTDAFWIJKING', + DSTDEVP: 'DBSTDAFWIJKINGP', DSUM: 'DBSOM', + DVAR: 'DBVAR', + DVARP: 'DBVARP', DDB: 'DDB', DEC2BIN: 'DEC.N.BIN', DEC2HEX: 'DEC.N.HEX', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 200d6694c..0b0db49cb 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'BD.MAX', DMIN: 'BD.MIN', DPRODUCT: 'BD.ILOCZYN', + DSTDEV: 'BD.ODCH.STANDARD', + DSTDEVP: 'BD.ODCH.STANDARD.POPUL', DSUM: 'BD.SUMA', + DVAR: 'BD.WARIANCJA', + DVARP: 'BD.WARIANCJA.POPUL', DDB: 'DDB', DEC2BIN: 'DZIES.NA.DWÓJK', DEC2HEX: 'DZIES.NA.SZESN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 4cc581e90..c512bb3a2 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'BDMÁX', DMIN: 'BDMÍN', DPRODUCT: 'BDPRODUTO', + DSTDEV: 'BDDESVPAD', + DSTDEVP: 'BDDESVPADP', DSUM: 'BDSOMA', + DVAR: 'BDVAR', + DVARP: 'BDVARP', DDB: 'BDD', DEC2BIN: 'DECABIN', DEC2HEX: 'DECAHEX', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 8a3d68877..654b28a50 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'БДМАКС', DMIN: 'БДМИН', DPRODUCT: 'БДПРОИЗВЕД', + DSTDEV: 'БДСТАНДОТКЛ', + DSTDEVP: 'БДСТАНДОТКЛП', DSUM: 'БДСУММ', + DVAR: 'БДДИСП', + DVARP: 'БДДИСПП', DDB: 'ДДОБ', DEC2BIN: 'ДЕС.В.ДВ', DEC2HEX: 'ДЕС.В.ШЕСТН', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 551bd31c5..bce2c4cf8 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'DMAX', DMIN: 'DMIN', DPRODUCT: 'DPRODUKT', + DSTDEV: 'DSTDAV', + DSTDEVP: 'DSTDAVP', DSUM: 'DSUMMA', + DVAR: 'DVARIANS', + DVARP: 'DVARIANSP', DDB: 'DEGAVSKR', DEC2BIN: 'DEC.TILL.BIN', DEC2HEX: 'DEC.TILL.HEX', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 150341739..e3658a8fb 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -82,7 +82,11 @@ const dictionary: RawTranslationPackage = { DMAX: 'VMAKS', DMIN: 'VMİN', DPRODUCT: 'VÇARPIM', + DSTDEV: 'VSTDSAPMA', + DSTDEVP: 'VSTDSAPMAP', DSUM: 'VTOPLA', + DVAR: 'VVAR', + DVARP: 'VVARP', DDB: 'ÇİFTAZALANBAKİYE', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 3dda34ca5..93f88fb43 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -32,7 +32,7 @@ type DatabaseCriteriaRow = DatabaseCriterionEntry[] /** * Interpreter plugin implementing Excel database functions. * - * Implements: DAVERAGE, DCOUNT, DCOUNTA, DGET, DMAX, DMIN, DPRODUCT, DSUM. + * Implements: DAVERAGE, DCOUNT, DCOUNTA, DGET, DMAX, DMIN, DPRODUCT, DSTDEV, DSTDEVP, DSUM, DVAR, DVARP. */ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginTypecheck { @@ -61,6 +61,22 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType {argumentType: FunctionArgumentType.RANGE}, ], }, + 'DSTDEV': { + method: 'dstdev', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DSTDEVP': { + method: 'dstdevp', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, 'DSUM': { method: 'dsum', parameters: [ @@ -69,6 +85,22 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType {argumentType: FunctionArgumentType.RANGE}, ], }, + 'DVAR': { + method: 'dvar', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, + 'DVARP': { + method: 'dvarp', + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, + ], + }, 'DAVERAGE': { method: 'daverage', parameters: [ @@ -413,6 +445,161 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType }) } + /** + * Returns the sample standard deviation of numeric values in the specified field + * of a database range, for rows that match all criteria. + * Returns #DIV/0! when fewer than 2 numeric values are found. + * + * DSTDEV(database, field, criteria) + */ + public dstdev(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DSTDEV'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + + if (values.length <= 1) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + + const mean = values.reduce((a, b) => a + b, 0) / values.length + const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) + return Math.sqrt(variance) + }) + } + + /** + * Returns the population standard deviation of numeric values in the specified field + * of a database range, for rows that match all criteria. + * Returns #DIV/0! when no numeric values are found. + * Returns 0 when exactly one numeric value is found. + * + * DSTDEVP(database, field, criteria) + */ + public dstdevp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DSTDEVP'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + + if (values.length === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + + const mean = values.reduce((a, b) => a + b, 0) / values.length + const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length + return Math.sqrt(variance) + }) + } + + /** + * Returns the sample variance of numeric values in the specified field + * of a database range, for rows that match all criteria. + * Returns #DIV/0! when fewer than 2 numeric values are found. + * + * DVAR(database, field, criteria) + */ + public dvar(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DVAR'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + + if (values.length <= 1) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + + const mean = values.reduce((a, b) => a + b, 0) / values.length + return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) + }) + } + + /** + * Returns the population variance of numeric values in the specified field + * of a database range, for rows that match all criteria. + * Returns #DIV/0! when no numeric values are found. + * Returns 0 when exactly one numeric value is found. + * + * DVARP(database, field, criteria) + */ + public dvarp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('DVARP'), + (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { + const fieldIndex = this.resolveFieldIndex(database, field) + if (fieldIndex instanceof CellError) { + return fieldIndex + } + + const criteriaRows = this.buildDatabaseCriteria(database, criteria) + if (criteriaRows instanceof CellError) { + return criteriaRows + } + + const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + + if (values.length === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } + + const mean = values.reduce((a, b) => a + b, 0) / values.length + return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length + }) + } + + /** + * Collects numeric values from the specified field column of matching database rows. + * + * @param dbData - Full database data including header row. + * @param fieldIndex - 0-based column index of the target field. + * @param criteriaRows - Parsed criteria rows. + * @returns Array of numeric values from matching rows. + */ + private collectNumericValues( + dbData: InternalScalarValue[][], + fieldIndex: number, + criteriaRows: DatabaseCriteriaRow[] + ): number[] { + const values: number[] = [] + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + values.push(getRawValue(cellValue) as number) + } + } + } + + return values + } + /** * Resolves the field argument to a 0-based column index within the database range. * From 8470ad090951199d136aed5920a8037e29ca26a2 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 15:12:36 +0000 Subject: [PATCH 08/27] Implement all 12 database functions (D-functions family) New DatabasePlugin with shared helpers for the complete D-function family: DCOUNT, DSUM, DAVERAGE, DMAX, DMIN, DGET, DPRODUCT, DCOUNTA, DSTDEV, DSTDEVP, DVAR, DVARP. 169 tests across 12 test suites. All pass. 17 language translations per function. Excel validation: 152/152 PASS (verified in Excel Desktop). Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2627823..f118c1d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Added 12 database functions: DCOUNT, DSUM, DAVERAGE, DMAX, DMIN, DGET, DPRODUCT, DCOUNTA, DSTDEV, DSTDEVP, DVAR, DVARP. [#1652](https://github.com/handsontable/hyperformula/pull/1652) - Added new functions: PERCENTILE, PERCENTILE.INC, PERCENTILE.EXC, QUARTILE, QUARTILE.INC, QUARTILE.EXC. [#1650](https://github.com/handsontable/hyperformula/pull/1650) - Added `maxPendingLazyTransformations` configuration option to control memory usage by limiting accumulated transformations before cleanup. [#1629](https://github.com/handsontable/hyperformula/issues/1629) - Added a new function: TEXTJOIN. [#1640](https://github.com/handsontable/hyperformula/pull/1640) - Added a new function: SEQUENCE. [#1645](https://github.com/handsontable/hyperformula/pull/1645) -- Added a new function: DCOUNT. (#1623) ### Fixed From 70906ef45ce5ac9f35b1216898696098024c0c78 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 10 Apr 2026 15:27:38 +0000 Subject: [PATCH 09/27] fix: remove unnecessary type assertions in DatabasePlugin (lint) --- src/interpreter/plugin/DatabasePlugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 93f88fb43..fffb8794b 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -232,7 +232,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - product *= getRawValue(cellValue) as number + product *= getRawValue(cellValue) hasNumeric = true } } @@ -268,7 +268,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - sum += getRawValue(cellValue) as number + sum += getRawValue(cellValue) } } } @@ -592,7 +592,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - values.push(getRawValue(cellValue) as number) + values.push(getRawValue(cellValue) ) } } } From b4fe776c9d5b4485ed1d5ad5ba3525b90ffd03c0 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 15 Apr 2026 12:27:01 +0000 Subject: [PATCH 10/27] Docs: add Database section to built-in-functions.md and nuances to known-limitations.md - Add ### Database section with all 12 D-functions (DAVERAGE, DCOUNT, DCOUNTA, DGET, DMAX, DMIN, DPRODUCT, DSTDEV, DSTDEVP, DSUM, DVAR, DVARP) to built-in-functions.md in alphabetical order between Date and Engineering - Add Database to TOC and remove "database" from "yet to be supported" intro - Add nuance notes for DGET (#VALUE!/#NUM! error semantics) and DMAX/DMIN/DPRODUCT (return 0 when no records match) Co-Authored-By: Claude Sonnet 4.6 --- docs/guide/built-in-functions.md | 21 +++++++++++++++++++-- docs/guide/known-limitations.md | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index e1ad66821..b1c998e60 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -28,6 +28,7 @@ The latest version of HyperFormula has an extensive collection of **{{ $page.functionsCount }}** functions grouped into categories: - [Array manipulation](#array-manipulation) +- [Database](#database) - [Date and time](#date-and-time) - [Engineering](#engineering) - [Information](#information) @@ -40,8 +41,7 @@ The latest version of HyperFormula has an extensive collection of - [Statistical](#statistical) - [Text](#text) -_Some categories such as compatibility, cube, and database are yet to be -supported._ +_Some categories such as compatibility and cube are yet to be supported._ ::: tip You can modify the built-in functions or create your own, by adding a [custom function](custom-functions). @@ -91,6 +91,23 @@ Total number of functions: **{{ $page.functionsCount }}** | YEAR | Returns the year as a number according to the internal calculation rules. | YEAR(Number) | | YEARFRAC | Computes the difference between two date values, in fraction of years. | YEARFRAC(Date2, Date1[, Format]) | +### Database + +| Function ID | Description | Syntax | +|:------------|:----------------------------------------------------------------------------------------------------------------|:----------------------------------| +| DAVERAGE | Returns the average of all values in a database field that match the given criteria. | DAVERAGE(Database, Field, Criteria) | +| DCOUNT | Counts the cells containing numbers in a database field that match the given criteria. | DCOUNT(Database, Field, Criteria) | +| DCOUNTA | Counts the non-empty cells in a database field that match the given criteria. | DCOUNTA(Database, Field, Criteria) | +| DGET | Returns the single value from a database field that matches the given criteria. Returns #VALUE! if no records match, and #NUM! if more than one record matches. | DGET(Database, Field, Criteria) | +| DMAX | Returns the maximum value in a database field that matches the given criteria. | DMAX(Database, Field, Criteria) | +| DMIN | Returns the minimum value in a database field that matches the given criteria. | DMIN(Database, Field, Criteria) | +| DPRODUCT | Returns the product of all values in a database field that match the given criteria. | DPRODUCT(Database, Field, Criteria) | +| DSTDEV | Returns the sample standard deviation of all values in a database field that match the given criteria. | DSTDEV(Database, Field, Criteria) | +| DSTDEVP | Returns the population standard deviation of all values in a database field that match the given criteria. | DSTDEVP(Database, Field, Criteria) | +| DSUM | Returns the sum of all values in a database field that match the given criteria. | DSUM(Database, Field, Criteria) | +| DVAR | Returns the sample variance of all values in a database field that match the given criteria. | DVAR(Database, Field, Criteria) | +| DVARP | Returns the population variance of all values in a database field that match the given criteria. | DVARP(Database, Field, Criteria) | + ### Engineering | Function ID | Description | Syntax | diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 72753b9b2..572905982 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -38,3 +38,5 @@ you can't compare the arguments in a formula like this: * 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. +* DGET returns `#VALUE!` when no records match the criteria, and `#NUM!` when more than one record matches. +* DMAX, DMIN, and DPRODUCT return `0` when no records match the criteria (consistent with Excel behavior). From e630479aeead6c1bc631ffb2ca4c132ed16de05f Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 16 Apr 2026 12:12:09 +0000 Subject: [PATCH 11/27] refactor: extract shared boilerplate in DatabasePlugin (DRY) Introduce databaseFunctionParameters constant and withDatabaseArgs() helper to eliminate repeated field/criteria resolution across all 12 database functions. Reduces file from 725 to 533 lines with no behavioral changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 554 ++++++++--------------- 1 file changed, 181 insertions(+), 373 deletions(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index fffb8794b..5e58f2d6a 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -29,6 +29,13 @@ interface DatabaseCriterionEntry { */ type DatabaseCriteriaRow = DatabaseCriterionEntry[] +/** Shared parameter signature for all database functions: (database RANGE, field SCALAR, criteria RANGE). */ +const databaseFunctionParameters = [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.RANGE}, +] + /** * Interpreter plugin implementing Excel database functions. * @@ -37,112 +44,31 @@ type DatabaseCriteriaRow = DatabaseCriterionEntry[] export class DatabasePlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { - 'DCOUNT': { - method: 'dcount', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DCOUNTA': { - method: 'dcounta', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DPRODUCT': { - method: 'dproduct', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DSTDEV': { - method: 'dstdev', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DSTDEVP': { - method: 'dstdevp', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DSUM': { - method: 'dsum', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DVAR': { - method: 'dvar', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DVARP': { - method: 'dvarp', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DAVERAGE': { - method: 'daverage', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DGET': { - method: 'dget', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DMAX': { - method: 'dmax', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, - 'DMIN': { - method: 'dmin', - parameters: [ - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.SCALAR}, - {argumentType: FunctionArgumentType.RANGE}, - ], - }, + 'DCOUNT': {method: 'dcount', parameters: databaseFunctionParameters}, + 'DCOUNTA': {method: 'dcounta', parameters: databaseFunctionParameters}, + 'DPRODUCT': {method: 'dproduct', parameters: databaseFunctionParameters}, + 'DSTDEV': {method: 'dstdev', parameters: databaseFunctionParameters}, + 'DSTDEVP': {method: 'dstdevp', parameters: databaseFunctionParameters}, + 'DSUM': {method: 'dsum', parameters: databaseFunctionParameters}, + 'DVAR': {method: 'dvar', parameters: databaseFunctionParameters}, + 'DVARP': {method: 'dvarp', parameters: databaseFunctionParameters}, + 'DAVERAGE': {method: 'daverage', parameters: databaseFunctionParameters}, + 'DGET': {method: 'dget', parameters: databaseFunctionParameters}, + 'DMAX': {method: 'dmax', parameters: databaseFunctionParameters}, + 'DMIN': {method: 'dmin', parameters: databaseFunctionParameters}, } /** - * Counts cells containing numbers in the specified field of a database range, - * for rows that match all criteria. - * - * DCOUNT(database, field, criteria) + * Resolves field index and criteria, then delegates to the callback with the parsed database arguments. + * Shared boilerplate for all 12 database functions. */ - public dcount(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DCOUNT'), + private withDatabaseArgs( + ast: ProcedureAst, + state: InterpreterState, + functionName: string, + callback: (dbData: InternalScalarValue[][], fieldIndex: number, criteriaRows: DatabaseCriteriaRow[]) => InternalScalarValue + ): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata(functionName), (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { const fieldIndex = this.resolveFieldIndex(database, field) if (fieldIndex instanceof CellError) { @@ -154,20 +80,30 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType return criteriaRows } - const dbData = database.data - let count = 0 + return callback(database.data, fieldIndex, criteriaRows) + }) + } - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - count++ - } + /** + * Counts cells containing numbers in the specified field of a database range, + * for rows that match all criteria. + * + * DCOUNT(database, field, criteria) + */ + public dcount(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.withDatabaseArgs(ast, state, 'DCOUNT', (dbData, fieldIndex, criteriaRows) => { + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + if (isExtendedNumber(dbData[rowIdx][fieldIndex])) { + count++ } } + } - return count - }) + return count + }) } /** @@ -177,32 +113,20 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DCOUNTA(database, field, criteria) */ public dcounta(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DCOUNTA'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let count = 0 - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (cellValue !== EmptyValue && cellValue !== undefined && cellValue !== null) { - count++ - } + return this.withDatabaseArgs(ast, state, 'DCOUNTA', (dbData, fieldIndex, criteriaRows) => { + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue !== EmptyValue && cellValue !== undefined && cellValue !== null) { + count++ } } + } - return count - }) + return count + }) } /** @@ -212,34 +136,22 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DPRODUCT(database, field, criteria) */ public dproduct(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DPRODUCT'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let product = 1 - let hasNumeric = false - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - product *= getRawValue(cellValue) - hasNumeric = true - } + return this.withDatabaseArgs(ast, state, 'DPRODUCT', (dbData, fieldIndex, criteriaRows) => { + let product = 1 + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + product *= getRawValue(cellValue) + hasNumeric = true } } + } - return hasNumeric ? product : 0 - }) + return hasNumeric ? product : 0 + }) } /** @@ -249,32 +161,20 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DSUM(database, field, criteria) */ public dsum(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DSUM'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let sum = 0 - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - sum += getRawValue(cellValue) - } + return this.withDatabaseArgs(ast, state, 'DSUM', (dbData, fieldIndex, criteriaRows) => { + let sum = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + sum += getRawValue(cellValue) } } + } - return sum - }) + return sum + }) } /** @@ -285,38 +185,26 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DAVERAGE(database, field, criteria) */ public daverage(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DAVERAGE'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let sum = 0 - let count = 0 - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - sum += getRawValue(cellValue) - count++ - } + return this.withDatabaseArgs(ast, state, 'DAVERAGE', (dbData, fieldIndex, criteriaRows) => { + let sum = 0 + let count = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + sum += getRawValue(cellValue) + count++ } } + } - if (count === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) - } + if (count === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } - return sum / count - }) + return sum / count + }) } /** @@ -327,40 +215,28 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DGET(database, field, criteria) */ public dget(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DGET'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let matchedValue: InternalScalarValue | undefined - let matchCount = 0 - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - matchCount++ - if (matchCount > 1) { - return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) - } - matchedValue = dbData[rowIdx][fieldIndex] + return this.withDatabaseArgs(ast, state, 'DGET', (dbData, fieldIndex, criteriaRows) => { + let matchedValue: InternalScalarValue | undefined + let matchCount = 0 + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + matchCount++ + if (matchCount > 1) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } + matchedValue = dbData[rowIdx][fieldIndex] } + } - if (matchCount === 0) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } + if (matchCount === 0) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } - return matchedValue === EmptyValue || matchedValue === undefined || matchedValue === null - ? 0 - : matchedValue - }) + return matchedValue === EmptyValue || matchedValue === undefined || matchedValue === null + ? 0 + : matchedValue + }) } /** @@ -371,37 +247,25 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DMAX(database, field, criteria) */ public dmax(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DMAX'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let max = -Infinity - let hasNumeric = false - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - const numValue = getRawValue(cellValue) - if (numValue > max) { - max = numValue - } - hasNumeric = true + return this.withDatabaseArgs(ast, state, 'DMAX', (dbData, fieldIndex, criteriaRows) => { + let max = -Infinity + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + const numValue = getRawValue(cellValue) + if (numValue > max) { + max = numValue } + hasNumeric = true } } + } - return hasNumeric ? max : 0 - }) + return hasNumeric ? max : 0 + }) } /** @@ -412,37 +276,25 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DMIN(database, field, criteria) */ public dmin(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DMIN'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const dbData = database.data - let min = Infinity - let hasNumeric = false - - for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { - if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (isExtendedNumber(cellValue)) { - const numValue = getRawValue(cellValue) - if (numValue < min) { - min = numValue - } - hasNumeric = true + return this.withDatabaseArgs(ast, state, 'DMIN', (dbData, fieldIndex, criteriaRows) => { + let min = Infinity + let hasNumeric = false + + for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { + if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (isExtendedNumber(cellValue)) { + const numValue = getRawValue(cellValue) + if (numValue < min) { + min = numValue } + hasNumeric = true } } + } - return hasNumeric ? min : 0 - }) + return hasNumeric ? min : 0 + }) } /** @@ -453,28 +305,17 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DSTDEV(database, field, criteria) */ public dstdev(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DSTDEV'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + return this.withDatabaseArgs(ast, state, 'DSTDEV', (dbData, fieldIndex, criteriaRows) => { + const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) - if (values.length <= 1) { - return new CellError(ErrorType.DIV_BY_ZERO) - } + if (values.length <= 1) { + return new CellError(ErrorType.DIV_BY_ZERO) + } - const mean = values.reduce((a, b) => a + b, 0) / values.length - const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) - return Math.sqrt(variance) - }) + const mean = values.reduce((a, b) => a + b, 0) / values.length + const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) + return Math.sqrt(variance) + }) } /** @@ -486,28 +327,17 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DSTDEVP(database, field, criteria) */ public dstdevp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DSTDEVP'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + return this.withDatabaseArgs(ast, state, 'DSTDEVP', (dbData, fieldIndex, criteriaRows) => { + const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) - if (values.length === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) - } + if (values.length === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } - const mean = values.reduce((a, b) => a + b, 0) / values.length - const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length - return Math.sqrt(variance) - }) + const mean = values.reduce((a, b) => a + b, 0) / values.length + const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length + return Math.sqrt(variance) + }) } /** @@ -518,27 +348,16 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DVAR(database, field, criteria) */ public dvar(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DVAR'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + return this.withDatabaseArgs(ast, state, 'DVAR', (dbData, fieldIndex, criteriaRows) => { + const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) - if (values.length <= 1) { - return new CellError(ErrorType.DIV_BY_ZERO) - } + if (values.length <= 1) { + return new CellError(ErrorType.DIV_BY_ZERO) + } - const mean = values.reduce((a, b) => a + b, 0) / values.length - return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) - }) + const mean = values.reduce((a, b) => a + b, 0) / values.length + return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1) + }) } /** @@ -550,27 +369,16 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * DVARP(database, field, criteria) */ public dvarp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('DVARP'), - (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => { - const fieldIndex = this.resolveFieldIndex(database, field) - if (fieldIndex instanceof CellError) { - return fieldIndex - } - - const criteriaRows = this.buildDatabaseCriteria(database, criteria) - if (criteriaRows instanceof CellError) { - return criteriaRows - } - - const values = this.collectNumericValues(database.data, fieldIndex, criteriaRows) + return this.withDatabaseArgs(ast, state, 'DVARP', (dbData, fieldIndex, criteriaRows) => { + const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) - if (values.length === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) - } + if (values.length === 0) { + return new CellError(ErrorType.DIV_BY_ZERO) + } - const mean = values.reduce((a, b) => a + b, 0) / values.length - return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length - }) + const mean = values.reduce((a, b) => a + b, 0) / values.length + return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length + }) } /** From e8cc25c8444ba1b9c060b3aadb7f3325e910e8c0 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 16 Apr 2026 13:08:56 +0000 Subject: [PATCH 12/27] fix: remove Excel mention from known-limitations, trailing space - Remove "(consistent with Excel behavior)" from known-limitations.md per review guideline: describe HF behavior only, no Excel references - Remove trailing space in collectNumericValues Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guide/known-limitations.md | 2 +- src/interpreter/plugin/DatabasePlugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 572905982..6b628db94 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -39,4 +39,4 @@ you can't compare the arguments in a formula like this: * 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. * DGET returns `#VALUE!` when no records match the criteria, and `#NUM!` when more than one record matches. -* DMAX, DMIN, and DPRODUCT return `0` when no records match the criteria (consistent with Excel behavior). +* DMAX, DMIN, and DPRODUCT return `0` when no records match the criteria. diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 5e58f2d6a..df3ddb0e9 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -400,7 +400,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] if (isExtendedNumber(cellValue)) { - values.push(getRawValue(cellValue) ) + values.push(getRawValue(cellValue)) } } } From e9560a02e91230c925337ae22bf721a0ad62dd87 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 16 Apr 2026 13:37:52 +0000 Subject: [PATCH 13/27] ci: retrigger after tests repo rebase Co-Authored-By: Claude Opus 4.6 (1M context) From 78f1daf7a191406b283b554cd2cbaac455c4f229 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 08:11:34 +0000 Subject: [PATCH 14/27] fix: Czech DCOUNT translation and boolean field coercion Addresses Cursor Bugbot findings on PR #1652: - csCZ.ts: DCOUNT was English 'DCOUNT' instead of localized 'DPOCET' (consistent with DCOUNTA = 'DPOCET2') - resolveFieldIndex: coerce boolean field arg to number (TRUE -> 1, FALSE -> 0) per Excel convention, matching ArithmeticHelper semantics Regression test added in hyperformula-tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/csCZ.ts | 2 +- src/interpreter/plugin/DatabasePlugin.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index dd72b170e..e163d9ab1 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -76,7 +76,7 @@ const dictionary: RawTranslationPackage = { DAYS: 'DAYS', DB: 'ODPIS.ZRYCH', DAVERAGE: 'DPRUMER', - DCOUNT: 'DCOUNT', + DCOUNT: 'DPOCET', DCOUNTA: 'DPOCET2', DGET: 'DZISKAT', DMAX: 'DMAX', diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index df3ddb0e9..6b9417b01 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -413,6 +413,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * * @param database - The database range (first row = headers). * @param field - A string (header name, case-insensitive) or number (1-based column index). + * Booleans are coerced to numbers (TRUE → 1, FALSE → 0) per Excel convention. * @returns 0-based column index, or CellError if field is invalid. */ private resolveFieldIndex(database: SimpleRangeValue, field: RawScalarValue): number | CellError { @@ -429,8 +430,10 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } - if (isExtendedNumber(field)) { - const index = Math.trunc(getRawValue(field)) + const numericField = typeof field === 'boolean' ? Number(field) : field + + if (isExtendedNumber(numericField)) { + const index = Math.trunc(getRawValue(numericField)) if (index < 1 || index > headers.length) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } From 7498108dbae22b02393bd14d92e335914172111c Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 08:30:08 +0000 Subject: [PATCH 15/27] fix: guard resolveFieldIndex against non-finite indices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Number.isFinite to reject NaN and Infinity before the bounds check — the previous `index < 1 || index > headers.length` comparison silently returns false for NaN, which would let a NaN index through and produce incorrect results. Addresses Cursor Bugbot finding. Regression test added in hyperformula-tests. --- src/interpreter/plugin/DatabasePlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 6b9417b01..86f965c21 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -434,7 +434,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType if (isExtendedNumber(numericField)) { const index = Math.trunc(getRawValue(numericField)) - if (index < 1 || index > headers.length) { + if (!Number.isFinite(index) || index < 1 || index > headers.length) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } return index - 1 From 96034e8de6ac4b784688bac65f70e6ee162b0fe2 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 08:51:45 +0000 Subject: [PATCH 16/27] fix: propagate CellError from field and criteria arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SCALAR argument types do not auto-propagate CellError in the framework (coerceArgumentsToRequiredTypes skips propagation for SCALAR), so the raw error reaches resolveFieldIndex and buildDatabaseCriteria. Both sites used to fall through into generic #VALUE! / BadCriterion, discarding the original error type. Now both check for CellError upfront and return it verbatim, preserving #DIV/0!, #NUM!, etc. — matching Excel's error-propagation behavior. Addresses Cursor Bugbot finding. Regression tests added in hyperformula-tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 86f965c21..de308b623 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -417,6 +417,10 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * @returns 0-based column index, or CellError if field is invalid. */ private resolveFieldIndex(database: SimpleRangeValue, field: RawScalarValue): number | CellError { + if (field instanceof CellError) { + return field + } + const headers = database.data[0] if (typeof field === 'string') { @@ -485,6 +489,11 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType continue } + // Propagate errors from the criteria cells instead of masking them as BadCriterion + if (criterionValue instanceof CellError) { + return criterionValue + } + const rawCriterionValue = isExtendedNumber(criterionValue) ? getRawValue(criterionValue) : criterionValue const criterionPackage = this.interpreter.criterionBuilder.fromCellValue( From ad18e293b04543b97b8fa0cad369672b62b63211 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 09:12:47 +0000 Subject: [PATCH 17/27] fix: Dutch DCOUNTA translation typo (DBAAANTALC -> DBAANTALC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dutch translation had a triple A typo — DCOUNT is DBAANTAL and the COUNTA variant with C suffix should be DBAANTALC. Addresses Cursor Bugbot finding. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/nlNL.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 98efe1d15..57d6fddb9 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -77,7 +77,7 @@ const dictionary: RawTranslationPackage = { DB: 'DB', DAVERAGE: 'DBGEMIDDELDE', DCOUNT: 'DBAANTAL', - DCOUNTA: 'DBAAANTALC', + DCOUNTA: 'DBAANTALC', DGET: 'DBLEZEN', DMAX: 'DBMAX', DMIN: 'DBMIN', From b77c822b981f2c873d9ac15650dc353881a2bd3f Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 09:14:57 +0000 Subject: [PATCH 18/27] fix: Norwegian DCOUNT translation (ANTALLDB -> DANTALL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous value lacked the D prefix and reversed the word order. Verified against Microsoft Support's official Norwegian Excel reference: DCOUNT → DANTALL. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/nbNO.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 495a36ccd..80d9948f6 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -76,7 +76,7 @@ const dictionary: RawTranslationPackage = { DAYS: 'DAGER', DB: 'DAVSKR', DAVERAGE: 'DGJENNOMSNITT', - DCOUNT: 'ANTALLDB', + DCOUNT: 'DANTALL', DCOUNTA: 'DANTALLA', DGET: 'DHENT', DMAX: 'DMAKS', From e599eae9bfcad114efc99fdc55a7826543e58225 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 09:40:21 +0000 Subject: [PATCH 19/27] fix: propagate CellError from field cells in D-function aggregations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isExtendedNumber silently skipped CellError values, letting numeric aggregation D-functions return a clean number even when a matching field cell contained #DIV/0! / #N/A / etc. Excel propagates the error instead. Each of DSUM, DPRODUCT, DAVERAGE, DMAX, DMIN now returns the error immediately when encountered. collectNumericValues (shared by DSTDEV, DSTDEVP, DVAR, DVARP) now returns CellError | number[] so those four propagate too. DCOUNT, DCOUNTA, and DGET are unchanged — COUNT-family semantics in Excel do not propagate field errors, and DGET already returns the cell verbatim. Regression test added in hyperformula-tests. Addresses Cursor Bugbot finding. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 35 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index de308b623..84f9b32fa 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -143,6 +143,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { product *= getRawValue(cellValue) hasNumeric = true @@ -167,6 +170,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { sum += getRawValue(cellValue) } @@ -192,6 +198,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { sum += getRawValue(cellValue) count++ @@ -254,6 +263,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { const numValue = getRawValue(cellValue) if (numValue > max) { @@ -283,6 +295,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { const numValue = getRawValue(cellValue) if (numValue < min) { @@ -307,6 +322,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType public dstdev(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.withDatabaseArgs(ast, state, 'DSTDEV', (dbData, fieldIndex, criteriaRows) => { const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) + if (values instanceof CellError) { + return values + } if (values.length <= 1) { return new CellError(ErrorType.DIV_BY_ZERO) @@ -329,6 +347,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType public dstdevp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.withDatabaseArgs(ast, state, 'DSTDEVP', (dbData, fieldIndex, criteriaRows) => { const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) + if (values instanceof CellError) { + return values + } if (values.length === 0) { return new CellError(ErrorType.DIV_BY_ZERO) @@ -350,6 +371,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType public dvar(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.withDatabaseArgs(ast, state, 'DVAR', (dbData, fieldIndex, criteriaRows) => { const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) + if (values instanceof CellError) { + return values + } if (values.length <= 1) { return new CellError(ErrorType.DIV_BY_ZERO) @@ -371,6 +395,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType public dvarp(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.withDatabaseArgs(ast, state, 'DVARP', (dbData, fieldIndex, criteriaRows) => { const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows) + if (values instanceof CellError) { + return values + } if (values.length === 0) { return new CellError(ErrorType.DIV_BY_ZERO) @@ -387,18 +414,22 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType * @param dbData - Full database data including header row. * @param fieldIndex - 0-based column index of the target field. * @param criteriaRows - Parsed criteria rows. - * @returns Array of numeric values from matching rows. + * @returns Array of numeric values from matching rows, or the first CellError + * encountered in a matching field cell (propagated per Excel semantics). */ private collectNumericValues( dbData: InternalScalarValue[][], fieldIndex: number, criteriaRows: DatabaseCriteriaRow[] - ): number[] { + ): number[] | CellError { const values: number[] = [] for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (isExtendedNumber(cellValue)) { values.push(getRawValue(cellValue)) } From 1dd7b103a0d63334729075502b0d88161140932a Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 09:50:30 +0000 Subject: [PATCH 20/27] fix: propagate CellError from field cells in DCOUNT and DCOUNTA too Extends the CellError propagation introduced in 947a9fdbf to DCOUNT and DCOUNTA. This keeps all 12 D-functions consistent: a matching field cell containing an error surfaces the error with its original type rather than being silently skipped or counted. Addresses Cursor Bugbot findings. Regression tests added in hyperformula-tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 84f9b32fa..b8649c6d7 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -96,7 +96,11 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - if (isExtendedNumber(dbData[rowIdx][fieldIndex])) { + const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } + if (isExtendedNumber(cellValue)) { count++ } } @@ -119,6 +123,9 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } if (cellValue !== EmptyValue && cellValue !== undefined && cellValue !== null) { count++ } From 32ba51881b16247077aa984fca781f8dabe28e65 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 10:05:51 +0000 Subject: [PATCH 21/27] fix: Polish diacritics + DGET CellError explicit guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plPL: DCOUNTA 'BD.ILE.REKORDOW.A' -> 'BD.ILE.REKORDÓW.A' (missing Ó, matches DCOUNT pattern; Cursor Bugbot finding). - plPL: DAVERAGE 'BD.SREDNIA' -> 'BD.ŚREDNIA' (missing Ś, per Microsoft Polish documentation; pre-emptive fix for the same class of issue). - DGET: explicit CellError check on the matched cell makes the error- propagation path consistent with the other D-functions (previously worked by accident via fall-through). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/plPL.ts | 4 ++-- src/interpreter/plugin/DatabasePlugin.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 0b0db49cb..251a35bba 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -75,9 +75,9 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DNI.360', DAYS: 'DNI', DB: 'DB', - DAVERAGE: 'BD.SREDNIA', + DAVERAGE: 'BD.ŚREDNIA', DCOUNT: 'BD.ILE.REKORDÓW', - DCOUNTA: 'BD.ILE.REKORDOW.A', + DCOUNTA: 'BD.ILE.REKORDÓW.A', DGET: 'BD.POLE', DMAX: 'BD.MAX', DMIN: 'BD.MIN', diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index b8649c6d7..57ad06430 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -237,11 +237,15 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { + const cellValue = dbData[rowIdx][fieldIndex] + if (cellValue instanceof CellError) { + return cellValue + } matchCount++ if (matchCount > 1) { return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } - matchedValue = dbData[rowIdx][fieldIndex] + matchedValue = cellValue } } From b7a15f24d638ce37df9a07c1a4fb4c32fcf16416 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 10:18:44 +0000 Subject: [PATCH 22/27] fix: correct D-function translations for cs, tr, da, pt per Microsoft docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proactive audit against Microsoft's official localized Excel documentation (https://support.microsoft.com/{lang}/office/...) caught systemic mismatches beyond what Cursor Bugbot had flagged so far. Czech (csCZ) — 7 corrections (diacritics + DSUM word): - DAVERAGE DPRUMER -> DPRŮMĚR - DCOUNT DPOCET -> DPOČET - DCOUNTA DPOCET2 -> DPOČET2 - DGET DZISKAT -> DZÍSKAT - DPRODUCT DSOUCIN -> DSOUČIN - DSTDEV DSMODCH.VYBER -> DSMODCH.VÝBĚR - DSUM DSOUCET -> DSUMA (entire word was wrong) - DVAR DVAR.VYBER -> DVAR.VÝBĚR Turkish (trTR) — 11 corrections (VSEÇ prefix across the family): - DAVERAGE VORTALAMA -> VSEÇORT - DCOUNT VSAY -> VSEÇSAY - DCOUNTA VSAYMA -> VSEÇSAYDOLU - DMAX VMAKS -> VSEÇMAK - DMIN VMİN -> VSEÇMİN - DPRODUCT VÇARPIM -> VSEÇÇARP - DSTDEV VSTDSAPMA -> VSEÇSTDSAPMA - DSTDEVP VSTDSAPMAP -> VSEÇSTDSAPMAS - DSUM VTOPLA -> VSEÇTOPLA - DVAR VVAR -> VSEÇVAR - DVARP VVARP -> VSEÇVARS (DGET stays as VAL — Microsoft Turkish keeps the short form.) Danish (daDK) — 3 corrections: - DAVERAGE DGENNEMSNIT -> DMIDDEL - DCOUNT ANTAL.DB -> DTÆL - DCOUNTA ANTAL.DBV -> DTÆLV Portuguese (ptPT) — 3 corrections: - DCOUNTA BDCONTARA -> BDCONTAR.VAL - DGET BDEXTRAIR -> BDOBTER - DPRODUCT BDPRODUTO -> BDMULTIPL All 24 changes verified directly against Microsoft's support pages. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/i18n/languages/csCZ.ts | 16 ++++++++-------- src/i18n/languages/daDK.ts | 6 +++--- src/i18n/languages/ptPT.ts | 6 +++--- src/i18n/languages/trTR.ts | 22 +++++++++++----------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index e163d9ab1..71ce56b14 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -75,17 +75,17 @@ const dictionary: RawTranslationPackage = { DAYS360: 'ROK360', DAYS: 'DAYS', DB: 'ODPIS.ZRYCH', - DAVERAGE: 'DPRUMER', - DCOUNT: 'DPOCET', - DCOUNTA: 'DPOCET2', - DGET: 'DZISKAT', + DAVERAGE: 'DPRŮMĚR', + DCOUNT: 'DPOČET', + DCOUNTA: 'DPOČET2', + DGET: 'DZÍSKAT', DMAX: 'DMAX', DMIN: 'DMIN', - DPRODUCT: 'DSOUCIN', - DSTDEV: 'DSMODCH.VYBER', + DPRODUCT: 'DSOUČIN', + DSTDEV: 'DSMODCH.VÝBĚR', DSTDEVP: 'DSMODCH', - DSUM: 'DSOUCET', - DVAR: 'DVAR.VYBER', + DSUM: 'DSUMA', + DVAR: 'DVAR.VÝBĚR', DVARP: 'DVAR', DDB: 'ODPIS.ZRYCH2', DEC2BIN: 'DEC2BIN', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index fbe402417..ea5a1e4ce 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -75,9 +75,9 @@ const dictionary: RawTranslationPackage = { DAYS360: 'DAGE360', DAYS: 'DAGE', DB: 'DB', - DAVERAGE: 'DGENNEMSNIT', - DCOUNT: 'ANTAL.DB', - DCOUNTA: 'ANTAL.DBV', + DAVERAGE: 'DMIDDEL', + DCOUNT: 'DTÆL', + DCOUNTA: 'DTÆLV', DGET: 'DHENT', DMAX: 'DMAKS', DMIN: 'DMIN', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index c512bb3a2..3195612f0 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -77,11 +77,11 @@ const dictionary: RawTranslationPackage = { DB: 'BD', DAVERAGE: 'BDMÉDIA', DCOUNT: 'BDCONTAR', - DCOUNTA: 'BDCONTARA', - DGET: 'BDEXTRAIR', + DCOUNTA: 'BDCONTAR.VAL', + DGET: 'BDOBTER', DMAX: 'BDMÁX', DMIN: 'BDMÍN', - DPRODUCT: 'BDPRODUTO', + DPRODUCT: 'BDMULTIPL', DSTDEV: 'BDDESVPAD', DSTDEVP: 'BDDESVPADP', DSUM: 'BDSOMA', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index e3658a8fb..af2c25390 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -75,18 +75,18 @@ const dictionary: RawTranslationPackage = { DAYS360: 'GÜN360', DAYS: 'GÜNSAY', DB: 'AZALANBAKİYE', - DAVERAGE: 'VORTALAMA', - DCOUNT: 'VSAY', - DCOUNTA: 'VSAYMA', + DAVERAGE: 'VSEÇORT', + DCOUNT: 'VSEÇSAY', + DCOUNTA: 'VSEÇSAYDOLU', DGET: 'VAL', - DMAX: 'VMAKS', - DMIN: 'VMİN', - DPRODUCT: 'VÇARPIM', - DSTDEV: 'VSTDSAPMA', - DSTDEVP: 'VSTDSAPMAP', - DSUM: 'VTOPLA', - DVAR: 'VVAR', - DVARP: 'VVARP', + DMAX: 'VSEÇMAK', + DMIN: 'VSEÇMİN', + DPRODUCT: 'VSEÇÇARP', + DSTDEV: 'VSEÇSTDSAPMA', + DSTDEVP: 'VSEÇSTDSAPMAS', + DSUM: 'VSEÇTOPLA', + DVAR: 'VSEÇVAR', + DVARP: 'VSEÇVARS', DDB: 'ÇİFTAZALANBAKİYE', DEC2BIN: 'DEC2BIN', DEC2HEX: 'DEC2HEX', From 2b080005f0971ba9b5d512575458a3d161cf85ab Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 17 Apr 2026 10:22:05 +0000 Subject: [PATCH 23/27] fix: DGET multiple-match check takes priority over field CellError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The defensive CellError guard added earlier returned the error as soon as a matching row with a field error was seen, even when more rows also matched. Excel's DGET resolves the "multiple matches → #NUM!" check first and only surfaces field errors when exactly one record matches. Move the CellError check to after the loop so the match count is finalised first. Addresses Cursor Bugbot finding. Regression tests added in hyperformula-tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/interpreter/plugin/DatabasePlugin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 57ad06430..91d3c8270 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -237,15 +237,11 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) { if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) { - const cellValue = dbData[rowIdx][fieldIndex] - if (cellValue instanceof CellError) { - return cellValue - } matchCount++ if (matchCount > 1) { return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } - matchedValue = cellValue + matchedValue = dbData[rowIdx][fieldIndex] } } @@ -253,6 +249,10 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } + if (matchedValue instanceof CellError) { + return matchedValue + } + return matchedValue === EmptyValue || matchedValue === undefined || matchedValue === null ? 0 : matchedValue From 936496ae1786b0addb58c8e4e7895640b5c47820 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 28 Apr 2026 05:54:50 +0000 Subject: [PATCH 24/27] Fix: HF-85 revert framework integration guide changes (out of scope) The Basic Usage sections added in earlier HF-85 commits duplicate the framework integration guides being delivered in PR #1653 (HF-122). Reverting them here keeps HF-85's scope strictly to D-functions and avoids three-way conflict when #1653 merges first. --- docs/guide/integration-with-angular.md | 64 -------------------------- docs/guide/integration-with-react.md | 56 ---------------------- docs/guide/integration-with-svelte.md | 46 ------------------ docs/guide/integration-with-vue.md | 42 ----------------- 4 files changed, 208 deletions(-) diff --git a/docs/guide/integration-with-angular.md b/docs/guide/integration-with-angular.md index 1e6b57998..8f78e2097 100644 --- a/docs/guide/integration-with-angular.md +++ b/docs/guide/integration-with-angular.md @@ -4,70 +4,6 @@ Installing HyperFormula in an Angular application works the same as with vanilla For more details, see the [client-side installation](client-side-installation.md) section. -## Basic usage - -### Step 1. Create a service - -Wrap HyperFormula in an Angular service. Create the instance in the constructor and expose methods for reading data. - -```typescript -import { Injectable, OnDestroy } from '@angular/core'; -import { HyperFormula } from 'hyperformula'; - -@Injectable({ providedIn: 'root' }) -export class SpreadsheetService implements OnDestroy { - private hf: HyperFormula; - private sheetId = 0; - - constructor() { - // Create a HyperFormula instance with initial data. - this.hf = HyperFormula.buildFromArray( - [ - [10, 20, '=SUM(A1:B1)'], - [30, 40, '=SUM(A2:B2)'], - ], - { licenseKey: 'gpl-v3' } - ); - } - - /** Return calculated values for the entire sheet. */ - getSheetValues(): (number | string | null)[][] { - return this.hf.getSheetValues(this.sheetId); - } - - ngOnDestroy(): void { - this.hf.destroy(); - } -} -``` - -### Step 2. Use the service in a component - -Inject the service and display the calculated values. - -```typescript -import { Component } from '@angular/core'; -import { SpreadsheetService } from './spreadsheet.service'; - -@Component({ - selector: 'app-spreadsheet', - template: ` - - - - -
{{ cell }}
- `, -}) -export class SpreadsheetComponent { - data: (number | string | null)[][]; - - constructor(private spreadsheet: SpreadsheetService) { - this.data = this.spreadsheet.getSheetValues(); - } -} -``` - ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/angular-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-react.md b/docs/guide/integration-with-react.md index 1591aec85..d4bc7fe75 100644 --- a/docs/guide/integration-with-react.md +++ b/docs/guide/integration-with-react.md @@ -4,62 +4,6 @@ Installing HyperFormula in a React application works the same as with vanilla Ja For more details, see the [client-side installation](client-side-installation.md) section. -## Basic usage - -### Step 1. Initialize HyperFormula - -Use `useRef` to hold the HyperFormula instance so it persists across re-renders. Initialize it inside a `useEffect` hook. - -```javascript -import { useRef, useEffect, useState } from 'react'; -import { HyperFormula } from 'hyperformula'; - -function SpreadsheetComponent() { - const hfRef = useRef(null); - const [sheetData, setSheetData] = useState([]); - - useEffect(() => { - // Create a HyperFormula instance with initial data. - hfRef.current = HyperFormula.buildFromArray( - [ - [10, 20, '=SUM(A1:B1)'], - [30, 40, '=SUM(A2:B2)'], - ], - { licenseKey: 'gpl-v3' } - ); - - // Read calculated values from the sheet. - const sheetId = 0; - setSheetData(hfRef.current.getSheetValues(sheetId)); - - return () => { - // Clean up the instance when the component unmounts. - hfRef.current?.destroy(); - }; - }, []); -``` - -### Step 2. Render the results - -Display the calculated values in a table. - -```javascript - return ( - - - {sheetData.map((row, rowIdx) => ( - - {row.map((cell, colIdx) => ( - - ))} - - ))} - -
{cell}
- ); -} -``` - ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/react-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-svelte.md b/docs/guide/integration-with-svelte.md index bd8aff85d..8b3a5f4b6 100644 --- a/docs/guide/integration-with-svelte.md +++ b/docs/guide/integration-with-svelte.md @@ -4,52 +4,6 @@ Installing HyperFormula in a Svelte application works the same as with vanilla J For more details, see the [client-side installation](client-side-installation.md) section. -## Basic usage - -### Step 1. Initialize HyperFormula - -Create the HyperFormula instance directly in the component's ` -``` - -### Step 2. Render the results - -Display the data in a table and trigger calculation with a button. - -```html - - - - {#each data as row, rowIdx} - - {#each row as cell, colIdx} - - {/each} - - {/each} -
{cell}
-``` - ## Demo Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=${$page.buildDateURIEncoded}). diff --git a/docs/guide/integration-with-vue.md b/docs/guide/integration-with-vue.md index 68afa5a8e..eaa104e0e 100644 --- a/docs/guide/integration-with-vue.md +++ b/docs/guide/integration-with-vue.md @@ -4,48 +4,6 @@ Installing HyperFormula in a Vue application works the same as with vanilla Java For more details, see the [client-side installation](client-side-installation.md) section. -## Basic usage - -### Step 1. Initialize HyperFormula - -Wrap the HyperFormula instance in `markRaw` to prevent Vue's reactivity system from converting it into a proxy (see [Troubleshooting](#vue-reactivity-issues) below). Use `ref` for the data you want to display. - -```javascript - -``` - -### Step 2. Render the results - -Display the calculated values in a template. - -```html - -``` - ## Troubleshooting ### Vue reactivity issues From bead1c36e6cfed0ebe9a74dce46d581b5688f3c6 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 28 Apr 2026 05:55:07 +0000 Subject: [PATCH 25/27] =?UTF-8?q?Fix:=20HF-85=20remove=20DGET/DMAX/DMIN/DP?= =?UTF-8?q?RODUCT=20bullets=20=E2=80=94=20these=20match=20Excel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per sequba review: DGET returning #VALUE! / #NUM! and DMAX/DMIN/DPRODUCT returning 0 for no-match are Excel-identical behaviors, not HyperFormula deviations. They do not belong in known-limitations.md. The SEQUENCE/FILTER parse-time-dimensions bullet remains — that one is a HyperFormula-specific limitation. --- docs/guide/known-limitations.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 6b628db94..72753b9b2 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -38,5 +38,3 @@ you can't compare the arguments in a formula like this: * 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. -* DGET returns `#VALUE!` when no records match the criteria, and `#NUM!` when more than one record matches. -* DMAX, DMIN, and DPRODUCT return `0` when no records match the criteria. From 09a0585024351088181a587ce040a3454f30c6ed Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 29 Apr 2026 05:49:20 +0000 Subject: [PATCH 26/27] Fix: HF-85 Russian DCOUNTA missing yo letter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Microsoft Excel Russian translations, DCOUNTA is БСЧЁТА (with Ё, not Е). The pattern is consistent with COUNT (СЧЁТ), COUNTA (СЧЁТЗ), and DCOUNT (БСЧЁТ) which all use Ё. The previous БСЧЕТА was a typo missing the ё diacritical mark. Cursor Bugbot finding (real bug, not historical re-flag — verified by cross-referencing with COUNT/COUNTA/DCOUNT existing translations and Microsoft Excel Russian docs convention). --- src/i18n/languages/ruRU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 654b28a50..cfbf51e59 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -77,7 +77,7 @@ const dictionary: RawTranslationPackage = { DB: 'ФУО', DAVERAGE: 'БДСРЕДНЕЕ', DCOUNT: 'БСЧЁТ', - DCOUNTA: 'БСЧЕТА', + DCOUNTA: 'БСЧЁТА', DGET: 'БИЗВЛЕЧЬ', DMAX: 'БДМАКС', DMIN: 'БДМИН', From 74c43972528c85c5594f5268af11734f8a08e6cd Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 29 Apr 2026 09:51:06 +0000 Subject: [PATCH 27/27] Fix: HF-85 propagate CellError from criteria header cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Cursor Bugbot review and Excel behavior — when a criteria header cell evaluates to a CellError (e.g., #REF!, #DIV/0!), the D-function should return that error rather than silently mapping the column to -1 and continuing. This aligns the header-cell behavior with the data cell behavior at the same location (lines 535-537), which already propagated CellError. --- src/interpreter/plugin/DatabasePlugin.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 91d3c8270..1208a0f17 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -502,6 +502,14 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType const criteriaData = criteria.data const criteriaHeaders = criteriaData[0] + // Propagate errors from criteria header cells (Excel returns the header + // error when the D-function is evaluated, instead of silently skipping). + for (const criteriaHeader of criteriaHeaders) { + if (criteriaHeader instanceof CellError) { + return criteriaHeader + } + } + // Map each criteria column to a database column index (or -1 if no match) const criteriaColumnMapping: number[] = criteriaHeaders.map(criteriaHeader => { if (typeof criteriaHeader !== 'string') {