diff --git a/.eslintignore b/.eslintignore index 37928812fe..ea6f691b24 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,11 +11,10 @@ src/interpreter/plugin/3rdparty *.config.js karma.* doc -test/unit/_setupFiles/*.js +test/_setupFiles/*.js # Auto-generated directories commonjs -coverage dist doc es diff --git a/.eslintrc.js b/.eslintrc.js index 27fe09a0cd..7d6108d36b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,7 @@ module.exports = { }, parserOptions: { tsconfigRootDir: __dirname, - project: './tsconfig.test.json', + project: './tsconfig.json', createDefaultProgram: true, }, extends: [ diff --git a/.github/codecov.yml b/.github/codecov.yml deleted file mode 100644 index d0bd2428bd..0000000000 --- a/.github/codecov.yml +++ /dev/null @@ -1,14 +0,0 @@ -codecov: - require_ci_to_pass: yes - -coverage: - range: 95..100 - round: down - precision: 2 - -comment: - layout: "reach, diff, flags, files" - behavior: new - require_changes: false - require_base: yes - require_head: yes diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml deleted file mode 100644 index f70b6b37de..0000000000 --- a/.github/workflows/performance.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Performance -permissions: - contents: read - pull-requests: write - -on: - pull_request: - types: - - opened - - reopened - - synchronize # the head branch is updated from the base branch, new commits are pushed to the head branch, or the base branch is changed - -jobs: - performance-test: - strategy: - matrix: - node-version: [ '22' ] - os: [ 'ubuntu-latest' ] - name: Test performance - runs-on: ${{ matrix.os }} - steps: - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@56899e050abffc08c2b3b61f3ec6a79a9dc3223d # https://github.com/actions/setup-node/releases/tag/v1.4.4 - with: - node-version: ${{ matrix.node-version }} - - - name: (base) Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # https://github.com/actions/checkout/releases/tag/v2.3.4 - with: - ref: ${{ github.event.pull_request.base.sha }} - - - name: (base) Install dependencies - run: | - npm ci - - - name: (base) Run performance tests - run: | - npm run benchmark:write-to-file base.json - - - name: (head) Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # https://github.com/actions/checkout/releases/tag/v2.3.4 - with: - clean: false - - - name: (head) Install dependencies - run: | - npm ci - - - name: (head) Run performance tests - run: | - npm run benchmark:write-to-file head.json - - - name: Compare the results - run: | - npm run benchmark:compare-benchmarks base.json head.json performance-report.md - - - name: Publish a comment - header - uses: marocchino/sticky-pull-request-comment@6804b5ad49d19c10c9ae7cf5057352f7ff333f31 # https://github.com/marocchino/sticky-pull-request-comment/tree/v1.6.0 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - message: | - ## Performance comparison of head (${{ github.event.pull_request.head.sha }}) vs base (${{ github.event.pull_request.base.sha }}) - - - name: Publish a comment - performance comparison report - uses: marocchino/sticky-pull-request-comment@6804b5ad49d19c10c9ae7cf5057352f7ff333f31 # https://github.com/marocchino/sticky-pull-request-comment/tree/v1.6.0 - with: - append: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - path: performance-report.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51a87c303c..c289fd5b77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,12 +36,7 @@ jobs: - name: Run tests run: | - npm run test:unit.ci -- --coverage - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@6004246f47ab62d32be025ce173b241cd84ac58e # https://github.com/codecov/codecov-action/releases/tag/v1.0.13 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + npm run test:ci browser-tests: strategy: diff --git a/.gitignore b/.gitignore index d4bb1b0997..5d6c7e54aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .idea/ .vscode /commonjs/ -/coverage/ /dist/ /doc/ /docs/api/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b86ebbd7..f5f3cd5669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [3.2.0] - 2026-02-19 + +### Added + +- Added a new function: IRR. [#1591](https://github.com/handsontable/hyperformula/issues/1591) +- Added a new function: N. [#1585](https://github.com/handsontable/hyperformula/issues/1585) +- Added a new function: VALUE. [#1592](https://github.com/handsontable/hyperformula/issues/1592) + +### Fixed + +- Fixed `Error Map maximum size exceeded` error when loading big spreadsheets. [#1602](https://github.com/handsontable/hyperformula/issues/1602) + +## [3.1.1] - 2025-12-18 + ### Fixed - Fixed an issue where cells were not recalculated after adding, removing and renaming sheets. [#1116](https://github.com/handsontable/hyperformula/issues/1116) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..5a11d26cda --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/Makefile b/Makefile index ea1c9af8db..5262600c8f 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,7 @@ setup: ## Setup project compile: ## Compile to javascript @npm run compile -test: ## Run tests - @npm run test - -unit: ## Run unit tests - @npm run test:unit +check: typecheck lint ## Check whether code is working correctly (types + lint) test-ci: ## Separate test configuration for CI environment @npm run test @@ -26,9 +22,6 @@ lint: ## Show linting errors lint-fix: ## Fix linting errors @npm run lint:fix -coverage: ## Run tests and show coverage - @npm run test:coverage - doc: ## Generate documentation @npm run typedoc:build @@ -65,6 +58,6 @@ verify-production-licenses: help: ## Show all make commands @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -.PHONY: test coverage benchmark doc servedoc +.PHONY: test doc servedoc .DEFAULT_GOAL := help diff --git a/README.md b/README.md index f82fba5e94..0573ebfe1e 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,16 @@ --- +> 🚀 **We're hiring!** Join HyperFormula team as a **Senior Software Engineer**. [See the role and apply](https://handsontable.traffit.com/public/an/4b09e1395bf8ea42ef86db4c4657992c2f48673d). + HyperFormula is a headless spreadsheet built in TypeScript, serving as both a parser and evaluator of spreadsheet formulas. It can be integrated into your browser or utilized as a service with Node.js as your back-end technology. ## What HyperFormula can be used for? + HyperFormula doesn't assume any existing user interface, making it a general-purpose library that can be used in various business applications. Here are some examples: +- Deterministic compute layer for AI & LLMs +- Calculated fields in CRM and ERP software - Custom spreadsheet-like app - Business logic builder - Forms and form builder diff --git a/docs/.vuepress/components/HiringBanner.vue b/docs/.vuepress/components/HiringBanner.vue new file mode 100644 index 0000000000..55207e6f57 --- /dev/null +++ b/docs/.vuepress/components/HiringBanner.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 6e95cc8a90..7ec541d842 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -11,6 +11,7 @@ const searchPattern = new RegExp('^/api', 'i'); module.exports = { title: 'HyperFormula (v' + HyperFormula.version + ')', description: 'HyperFormula is an open-source, high-performance calculation engine for spreadsheets and web applications.', + globalUIComponents: ['HiringBanner'], head: [ // Import HF (required for the examples) [ 'script', { src: 'https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.full.min.js' } ], @@ -40,7 +41,7 @@ module.exports = { new Sentry.Replay({ maskAllText: false, blockAllMedia: false, - }), + }), ], }); }; @@ -62,6 +63,11 @@ module.exports = { ], base: '/', plugins: [ + ['sitemap', { + hostname: 'https://hyperformula.handsontable.com', + exclude: ['/404.html'], + changefreq: 'weekly' + }], searchBoxPlugin, ['container', examples()], { diff --git a/docs/.vuepress/public/robots.txt b/docs/.vuepress/public/robots.txt new file mode 100644 index 0000000000..ef083139b3 --- /dev/null +++ b/docs/.vuepress/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://hyperformula.handsontable.com/sitemap.xml diff --git a/docs/guide/building.md b/docs/guide/building.md index a781b05b8b..7827896429 100644 --- a/docs/guide/building.md +++ b/docs/guide/building.md @@ -68,10 +68,9 @@ pass in both of them because the library might be used to be sure that both environments are fine. * `npm run test` - runs the linter and all tests -* `npm run test:unit` - runs unit tests - * To run a test suite that matches a word, add a Jest `-t` flag. For example: `npm run test:unit -- -t 'SUMIF'` runs only the tests that match the word `SUMIF` within `describe()` or `it()`. - * To run a specific test suite, pass the file name. For example: `npm run test:unit 'function-sumif.spec.ts'` runs only the unit tests from the file `function-sumif.spec.ts`. -* `npm run test:coverage` - runs unit tests and generates code coverage +* `npm run test:jest` - runs unit tests + * To run a test suite that matches a word, add a Jest `-t` flag. For example: `npm run test:jest -- -t 'SUMIF'` runs only the tests that match the word `SUMIF` within `describe()` or `it()`. + * To run a specific test suite, pass the file name. For example: `npm run test:jest 'function-sumif.spec.ts'` runs only the unit tests from the file `function-sumif.spec.ts`. * `npm run test:browser` - runs tests in **karma** once and closes all open browsers * To run a specific `spec` file or a test suite you can add a Karma `--spec` flag. For example: `npm run test:browser.debug -- --spec=matrix.spec.ts` runs `matrix.spec.ts` browser tests only * `npm run test:browser.debug` - runs test in **karma** only in Chrome until you exit the process. It watches changes in `src` and `test` directories and rebuilds them automatically. diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 07d4075c62..dd97775232 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -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) | @@ -491,6 +492,7 @@ Total number of functions: **{{ $page.functionsCount }}** | LEN | Returns length of a given text. | LEN("Text") | | LOWER | Returns text converted to lowercase. | LOWER(Text) | | MID | Returns substring of a given length starting from Start_position. | MID(Text, Start_position, Length) | +| N | Converts a value to a number. | N(Value) | | PROPER | Capitalizes words given text string. | PROPER("Text") | | REPLACE | Replaces substring of a text of a given length that starts at given position. | REPLACE(Text, Start_position, Length, New_text) | | REPT | Repeats text a given number of times. | REPT("Text", Number) | @@ -504,6 +506,7 @@ Total number of functions: **{{ $page.functionsCount }}** | UNICHAR | Returns the character created by using provided code point. | UNICHAR(Number) | | UNICODE | Returns the Unicode code point of a first character of a text. | UNICODE(Text) | | UPPER | Returns text converted to uppercase. | UPPER(Text) | +| VALUE | Parses a number, date, time, datetime, currency, or percentage from a text string. | VALUE(Text) | [^non-odff]: The return value of this function is compliant with the diff --git a/docs/guide/custom-functions.md b/docs/guide/custom-functions.md index c19f3494e7..d78408e61d 100644 --- a/docs/guide/custom-functions.md +++ b/docs/guide/custom-functions.md @@ -363,7 +363,7 @@ This demo contains the implementation of both the [`DOUBLE_RANGE`](#advanced-custom-function-example) custom functions.