Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Added a new function: IRR. [#1591](https://github.com/handsontable/hyperformula/issues/1591)

## [3.1.1] - 2025-12-18

### Fixed
Expand Down
95 changes: 95 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

HyperFormula is a headless spreadsheet engine written in TypeScript. It parses and evaluates Excel-compatible formulas and can run in browser or Node.js environments. The library implements ~400 built-in functions with support for custom functions, undo/redo, CRUD operations, and i18n (17 languages).

## Build & Development Commands

```bash
npm install # Install dependencies
npm run compile # TypeScript compilation to lib/
npm run bundle-all # Full build: compile + bundle all formats
npm run lint # Run ESLint
npm run lint:fix # Auto-fix lint issues
```

## Testing

```bash
npm test # Full suite: lint + unit + browser + compatibility
npm run test:unit # Jest unit tests only
npm run test:watch # Jest watch mode (run tests on file changes)
npm run test:coverage # Unit tests with coverage report
npm run test:browser # Karma browser tests (Chrome/Firefox)
npm run test:performance # Run performance benchmarks
npm run test:compatibility # Excel compatibility tests
```

Test files are located in `test/unit/` and follow the pattern `*.spec.ts`.

## Architecture

### Core Components

- **`src/HyperFormula.ts`** - Main engine class, public API entry point
- **`src/parser/`** - Formula parsing using Chevrotain parser generator
- **`src/interpreter/`** - Formula evaluation engine
- **`src/DependencyGraph/`** - Cell dependency tracking and recalculation order
- **`src/CrudOperations.ts`** - Create/Read/Update/Delete operations on sheets and cells

### Function Plugins (`src/interpreter/plugin/`)

All spreadsheet functions are implemented as plugins extending `FunctionPlugin`. Each plugin:
- Declares `implementedFunctions` static property mapping function names to metadata
- Uses `runFunction()` helper for argument validation, coercion, and array handling
- Registers function translations in `src/i18n/languages/`

To add a new function:
1. Create or modify a plugin in `src/interpreter/plugin/`
2. Add function metadata to `implementedFunctions`
3. Implement the function method
4. Add translations to all language files in `src/i18n/languages/`
5. Add tests in `test/unit/interpreter/`

### i18n (`src/i18n/languages/`)

Function name translations for each supported language. When adding new functions, translations can be found at:
- https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889
- http://dolf.trieschnigg.nl/excel/index.php

## Output Formats

The build produces multiple output formats:
- `commonjs/` - CommonJS modules (main entry)
- `es/` - ES modules (.mjs files)
- `dist/` - UMD bundles for browsers
- `typings/` - TypeScript declaration files

## Contributing Guidelines

- Create feature branches, never commit directly to master
- Target the `develop` branch for pull requests
- Add tests for all changes in `test/` folder
- Run linter before submitting (`npm run lint`)
- Maintain compatibility with Excel and Google Sheets behavior
- In documentation, commit messages, pull request descriptions and code comments, do not mention Claude Code nor LLM models used for code generation

## Response Guidelines

- By default speak ultra-concisely, using as few words as you can, unless asked otherwise.
- Focus solely on instructions and provide relevant responses.
- Ask questions to remove ambiguity and make sure you're speaking about the right thing.
- Ask questions if you need more information to provide an accurate answer.
- If you don't know something, simply say, "I don't know," and ask for help.
- Present your answer in a structured way, use bullet lists, numbered lists, tables, etc.
- When asked for specific content, start the response with the requested info immediately.
- When answering based on context, support your claims by quoting exact fragments of available documents.

## Code Style

- When generating code, prefer functional approach whenever possible (in JS/TS use filter, map and reduce functions).
- Make the code self-documenting. Use meaningfull names for classes, functions, valiables etc. Add code comments only when necessary.
- Add jsdocs to all classes and functions.
1 change: 1 addition & 0 deletions docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Total number of functions: **{{ $page.functionsCount }}**
| FV | Returns the future value of an investment. | FV(Rate, Nper, Pmt[, Pv,[ Type]]) |
| FVSCHEDULE | Returns the future value of an investment based on a rate schedule. | FV(Pv, Schedule) |
| IPMT | Returns the interest portion of a given loan payment in a given payment period. | IPMT(Rate, Per, Nper, Pv[, Fv[, Type]]) |
| IRR | Returns the internal rate of return for a series of cash flows. | IRR(Values[, Guess]) |
| ISPMT | Returns the interest paid for a given period of an investment with equal principal payments. | ISPMT(Rate, Per, Nper, Value) |
| MIRR | Returns modified internal value for cashflows. | MIRR(Flows, FRate, RRate) |
| NOMINAL | Returns the nominal interest rate. | NOMINAL(Effect_rate, Npery) |
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'CELÁ.ČÁST',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'PLATBA.ÚROK',
IRR: 'MÍRA.VÝNOSNOSTI',
ISBINARY: 'ISBINARY',
ISBLANK: 'JE.PRÁZDNÉ',
ISERR: 'JE.CHYBA',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'HELTAL',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'R.YDELSE',
IRR: 'IA',
ISBINARY: 'ISBINARY',
ISBLANK: 'ER.TOM',
ISERR: 'ER.FJL',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'GANZZAHL',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'ZINSZ',
IRR: 'IKV',
ISBINARY: 'ISBINARY',
ISBLANK: 'ISTLEER',
ISERR: 'ISTFEHL',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const dictionary: RawTranslationPackage = {
INT: 'INT',
INTERVAL: 'INTERVAL',
IPMT: 'IPMT',
IRR: 'IRR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ISBLANK',
ISERR: 'ISERR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const dictionary: RawTranslationPackage = {
INT: 'ENTERO',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'PAGOINT',
IRR: 'TIR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ESBLANCO',
ISERR: 'ESERR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'KOKONAISLUKU',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'IPMT',
IRR: 'SISÄINEN.KORKO',
ISBINARY: 'ISBINARY',
ISBLANK: 'ONTYHJÄ',
ISERR: 'ONVIRH',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'ENT',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'INTPER',
IRR: 'TRI',
ISBINARY: 'ISBINARY',
ISBLANK: 'ESTVIDE',
ISERR: 'ESTERR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'INT',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'RRÉSZLET',
IRR: 'BMR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ÜRES',
ISERR: 'HIBA.E',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'INT',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'INTERESSI',
IRR: 'TIR.COST',
ISBINARY: 'ISBINARY',
ISBLANK: 'VAL.VUOTO',
ISERR: 'VAL.ERR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'HELTALL',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'RAVDRAG',
IRR: 'IR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ERTOM',
ISERR: 'ERF',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'INTEGER',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'IBET',
IRR: 'IR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ISLEEG',
ISERR: 'ISFOUT2',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'ZAOKR.DO.CAŁK',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'IPMT',
IRR: 'IRR',
ISBINARY: 'ISBINARY',
ISBLANK: 'CZY.PUSTA',
ISERR: 'CZY.BŁ',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'INT',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'IPGTO',
IRR: 'TIR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ÉCÉL.VAZIA',
ISERR: 'ÉERRO',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'ЦЕЛОЕ',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'ПРПЛТ',
IRR: 'ВСД',
ISBINARY: 'ISBINARY',
ISBLANK: 'ЕПУСТО',
ISERR: 'ЕОШ',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'HELTAL',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'RBETALNING',
IRR: 'IR',
ISBINARY: 'ISBINARY',
ISBLANK: 'ÄRTOM',
ISERR: 'ÄRF',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const dictionary: RawTranslationPackage = {
INT: 'TAMSAYI',
INTERVAL: 'INTERVAL', //FIXME
IPMT: 'FAİZTUTARI',
IRR: 'İÇ_VERİM_ORANI',
ISBINARY: 'ISBINARY',
ISBLANK: 'EBOŞSA',
ISERR: 'EHATA',
Expand Down
95 changes: 95 additions & 0 deletions src/interpreter/plugin/FinancialPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp
{argumentType: FunctionArgumentType.RANGE},
],
},
'IRR': {
method: 'irr',
parameters: [
{argumentType: FunctionArgumentType.RANGE},
{argumentType: FunctionArgumentType.NUMBER, defaultValue: 0.1},
],
returnNumberType: NumberType.NUMBER_PERCENT
},
}

public pmt(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
Expand Down Expand Up @@ -750,6 +758,36 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp
}
)
}

/**
* Calculates the internal rate of return for a series of cash flows.
* @param {ProcedureAst} ast - The AST node representing the function call.
* @param {InterpreterState} state - The interpreter state.
* @returns {InterpreterValue} The internal rate of return.
*/
public irr(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
return this.runFunction(ast.args, state, this.metadata('IRR'),
(range: SimpleRangeValue, guess: number) => {
if (guess <= -1) {
return new CellError(ErrorType.VALUE)
}

const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner())
if (vals instanceof CellError) {
return vals
}

// Check for at least one positive and one negative value
const hasPositive = vals.some(val => val > 0)
const hasNegative = vals.some(val => val < 0)
if (!hasPositive || !hasNegative) {
return new CellError(ErrorType.NUM)
}

return irrCore(vals, guess)
}
)
}
}

function pmtCore(rate: number, periods: number, present: number, future: number, type: number): number {
Expand Down Expand Up @@ -798,3 +836,60 @@ function npvCore(rate: number, args: number[]): number | CellError {
}
return acc
}

/**
* Calculates IRR using Newton-Raphson method.
* IRR is the rate r where: CF0 + CF1/(1+r) + CF2/(1+r)^2 + ... + CFn/(1+r)^n = 0
*/
function irrCore(values: number[], guess: number): number | CellError {
const epsMax = 1e-10
const iterMax = 50

let rate = guess

for (let iter = 0; iter < iterMax; iter++) {
// Calculate NPV and its derivative at current rate
// NPV = sum of values[i] / (1+rate)^i for i = 0 to n-1
// dNPV/dr = sum of -i * values[i] / (1+rate)^(i+1) for i = 0 to n-1
let npv = 0
let dnpv = 0

for (let i = 0; i < values.length; i++) {
const factor = Math.pow(1 + rate, i)
if (!isFinite(factor) || factor === 0) {
return new CellError(ErrorType.NUM)
}
npv += values[i] / factor
if (i > 0) {
dnpv -= i * values[i] / (factor * (1 + rate))
}
}

// Check for convergence
if (Math.abs(npv) < epsMax) {
return rate
}

// Check if derivative is too small (avoid division by zero)
if (Math.abs(dnpv) < epsMax) {
return new CellError(ErrorType.NUM)
}

// Newton-Raphson step
const newRate = rate - npv / dnpv

// Check for convergence based on rate change
if (Math.abs(newRate - rate) < epsMax) {
return newRate
}

rate = newRate

// Check for invalid rate
if (!isFinite(rate) || rate <= -1) {
return new CellError(ErrorType.NUM)
}
}

return new CellError(ErrorType.NUM)
}
Loading
Loading