diff --git a/.github/workflows/typedoc.yml b/.github/workflows/typedoc.yml index 881ec7f..81c1ff6 100644 --- a/.github/workflows/typedoc.yml +++ b/.github/workflows/typedoc.yml @@ -1,32 +1,15 @@ -name: Deploy TypeDoc on GitHub pages +name: TypeDoc on: workflow_dispatch: release: types: [published] -env: - NODE_VERSION: 20.x - ENTRY_FILE: 'src/index.ts' - jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install dependencies - run: npm install - - name: Build documentation - uses: zakodium/typedoc-action@v2 - with: - entry: ${{ env.ENTRY_FILE }} - - name: Deploy to GitHub pages - uses: JamesIves/github-pages-deploy-action@releases/v4 - with: - token: ${{ secrets.BOT_TOKEN }} - branch: gh-pages - folder: docs - clean: true + typedoc: + # Documentation: https://github.com/zakodium/workflows#typedoc + uses: zakodium/workflows/.github/workflows/typedoc.yml@typedoc-v1 + with: + entry: 'src/index.ts' + secrets: + github-token: ${{ secrets.BOT_TOKEN }} diff --git a/.gitignore b/.gitignore index b2c0bef..b942be8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,5 @@ jspm_packages .node_repl_history lib - -lib-esm docs +.claude diff --git a/eslint.config.mjs b/eslint.config.mjs index 5935144..889a2e4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,4 @@ import { defineConfig, globalIgnores } from 'eslint/config'; import ts from 'eslint-config-cheminfo-typescript/base'; -export default defineConfig(globalIgnores(['coverage', 'lib', 'lib-esm']), ts); +export default defineConfig(globalIgnores(['coverage', 'lib']), ts); diff --git a/package.json b/package.json index e5be536..af422d4 100644 --- a/package.json +++ b/package.json @@ -2,28 +2,27 @@ "name": "ml-regression-base", "version": "4.0.1", "description": "Base class for regression modules", - "main": "./lib/index.js", - "module": "./lib-esm/index.js", - "types": "./lib/index.d.ts", + "type": "module", + "exports": { + ".": "./lib/index.js" + }, "sideEffects": false, "files": [ - "src", "lib", - "lib-esm" + "src" ], "scripts": { "check-types": "tsc --noEmit", - "clean": "rimraf lib lib-esm", + "clean": "rimraf lib", "eslint": "eslint src", "eslint-fix": "npm run eslint -- --fix", "prepack": "npm run tsc", "prettier": "prettier --check src", "prettier-write": "prettier --write src", - "test": "npm run test-only && npm run eslint && npm run prettier && npm run check-types", + "test": "npm run test-only && npm run check-types && npm run eslint && npm run prettier", "test-only": "vitest run --coverage", - "tsc": "npm run clean && npm run tsc-cjs && npm run tsc-esm", - "tsc-cjs": "tsc --project tsconfig.cjs.json", - "tsc-esm": "tsc --project tsconfig.esm.json" + "tsc": "npm run clean && npm run tsc-build", + "tsc-build": "tsc --project tsconfig.build.json" }, "repository": { "type": "git", @@ -41,7 +40,9 @@ "is-any-array": "^3.0.0" }, "devDependencies": { + "@types/node": "^25.6.0", "@vitest/coverage-v8": "^4.1.4", + "@zakodium/tsconfig": "^1.0.5", "eslint": "^9.39.4", "eslint-config-cheminfo-typescript": "^22.0.0", "prettier": "^3.8.3", diff --git a/src/BaseRegression.ts b/src/BaseRegression.ts index ddd8026..bd98175 100644 --- a/src/BaseRegression.ts +++ b/src/BaseRegression.ts @@ -1,7 +1,7 @@ import type { NumberArray } from 'cheminfo-types'; import { isAnyArray } from 'is-any-array'; -import { checkArrayLength } from './checkArrayLength'; +import { checkArrayLength } from './checkArrayLength.ts'; export interface RegressionScore { r: number; @@ -9,6 +9,7 @@ export interface RegressionScore { chi2: number; rmsd: number; } + export class BaseRegression { constructor() { if (new.target === BaseRegression) { @@ -55,7 +56,7 @@ export class BaseRegression { * Return the correlation coefficient of determination (r) and chi-square. * @param x - explanatory variable * @param y - response variable - * @return - Object with further statistics. + * @returns Object with further statistics. */ score(x: NumberArray, y: NumberArray): RegressionScore { checkArrayLength(x, y); @@ -63,7 +64,7 @@ export class BaseRegression { const n = x.length; const y2: number[] = new Array(n); for (let i = 0; i < n; i++) { - y2[i] = this._predict(x[i]); + y2[i] = this._predict(x[i] as number); } let xSum = 0; @@ -74,15 +75,17 @@ export class BaseRegression { let ySquared = 0; let xY = 0; for (let i = 0; i < n; i++) { - xSum += y2[i]; - ySum += y[i]; - xSquared += y2[i] * y2[i]; - ySquared += y[i] * y[i]; - xY += y2[i] * y[i]; - if (y[i] !== 0) { - chi2 += ((y[i] - y2[i]) * (y[i] - y2[i])) / y[i]; + const yi = y[i] as number; + const y2i = y2[i] as number; + xSum += y2i; + ySum += yi; + xSquared += y2i * y2i; + ySquared += yi * yi; + xY += y2i * yi; + if (yi !== 0) { + chi2 += ((yi - y2i) * (yi - y2i)) / yi; } - rmsd += (y[i] - y2[i]) * (y[i] - y2[i]); + rmsd += (yi - y2i) * (yi - y2i); } const r = diff --git a/src/__tests__/BaseRegression.test.ts b/src/__tests__/BaseRegression.test.ts index 01aebf1..350c848 100644 --- a/src/__tests__/BaseRegression.test.ts +++ b/src/__tests__/BaseRegression.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from 'vitest'; +import { expect, test } from 'vitest'; -import { BaseRegression } from '..'; +import { BaseRegression } from '../index.ts'; class NoPredict extends BaseRegression {} class Basic extends BaseRegression { @@ -9,82 +9,86 @@ class Basic extends BaseRegression { super(); this.factor = factor; } - _predict(x: number) { + override _predict(x: number) { return x * this.factor; } } -describe('base regression', () => { - it('should not be directly constructable', () => { - expect(() => { - // eslint-disable-next-line no-new - new BaseRegression(); - }).toThrow(/BaseRegression must be subclassed/); - }); +test('should not be directly constructable', () => { + expect(() => { + // eslint-disable-next-line no-new + new BaseRegression(); + }).toThrow(/BaseRegression must be subclassed/); +}); - it('should throw if _predict is not implemented', () => { - const reg = new NoPredict(); - expect(() => { - reg.predict(0); - }).toThrow(/_predict must be implemented/); - }); +test('should throw if _predict is not implemented', () => { + const reg = new NoPredict(); - it('should do a basic prediction', () => { - const basic = new Basic(2); - expect(basic.predict(1)).toBe(2); - expect(basic.predict(2)).toBe(4); - expect(basic.predict([2, 3])).toStrictEqual([4, 6]); - }); + expect(() => { + reg.predict(0); + }).toThrow(/_predict must be implemented/); +}); + +test('should do a basic prediction', () => { + const basic = new Basic(2); + + expect(basic.predict(1)).toBe(2); + expect(basic.predict(2)).toBe(4); + expect(basic.predict([2, 3])).toStrictEqual([4, 6]); +}); + +test('should throw on invalid value', () => { + const basic = new Basic(2); - it('should throw on invalid value', () => { - const basic = new Basic(2); - expect(() => { - // @ts-expect-error testing invalid input - basic.predict(); - }).toThrow(/must be a number or array/); + expect(() => { + // @ts-expect-error testing invalid input + basic.predict(); + }).toThrow(/must be a number or array/); +}); + +test('should implement dummy predictor functions', () => { + const basic = new Basic(2); + basic.train(); // should not throw + + expect(basic.toString()).toBe(''); + expect(basic.toLaTeX()).toBe(''); +}); + +test('should implement a scoring function', () => { + const basic = new Basic(2); + + expect(basic.score([1, 2], [2, 4])).toStrictEqual({ + r: 1, + r2: 1, + chi2: 0, + rmsd: 0, }); + expect(basic.score([1, 2], [2, 4.1]).rmsd).toBe(0.0707106781186545); - it('should implement dummy predictor functions', () => { - const basic = new Basic(2); - basic.train(); // should not throw - expect(basic.toString()).toBe(''); - expect(basic.toLaTeX()).toBe(''); + expect(basic.score([1, 2], [0.5, 2])).toStrictEqual({ + r: 1, + r2: 1, + chi2: 6.5, + rmsd: 1.7677669529663689, }); - it('should implement a scoring function', () => { - const basic = new Basic(2); - expect(basic.score([1, 2], [2, 4])).toStrictEqual({ - r: 1, - r2: 1, - chi2: 0, - rmsd: 0, - }); - expect(basic.score([1, 2], [2, 4.1]).rmsd).toBe(0.0707106781186545); - - expect(basic.score([1, 2], [0.5, 2])).toStrictEqual({ - r: 1, - r2: 1, - chi2: 6.5, - rmsd: 1.7677669529663689, - }); - - expect(basic.score([1, 2], [0, 1])).toStrictEqual({ - r: 1, - r2: 1, - chi2: 9, - rmsd: 2.5495097567963922, - }); + expect(basic.score([1, 2], [0, 1])).toStrictEqual({ + r: 1, + r2: 1, + chi2: 9, + rmsd: 2.5495097567963922, }); +}); - it('should throw error if inputs are not arrays or has different length', () => { - const basic = new Basic(2); - expect(() => { - // @ts-expect-error testing invalid input - basic.score(1, 2); - }).toThrow('x and y must be arrays'); +test('should throw error if inputs are not arrays or has different length', () => { + const basic = new Basic(2); - expect(() => { - basic.score([1, 2], [2]); - }).toThrow('x and y arrays must have the same length'); - }); + expect(() => { + // @ts-expect-error testing invalid input + basic.score(1, 2); + }).toThrow('x and y must be arrays'); + + expect(() => { + basic.score([1, 2], [2]); + }).toThrow('x and y arrays must have the same length'); }); diff --git a/src/__tests__/checkArrayLength.test.ts b/src/__tests__/checkArrayLength.test.ts index a30ecf7..53b8f71 100644 --- a/src/__tests__/checkArrayLength.test.ts +++ b/src/__tests__/checkArrayLength.test.ts @@ -1,34 +1,34 @@ -import { describe, expect, it } from 'vitest'; +import { expect, test } from 'vitest'; -import { checkArrayLength } from '..'; +import { checkArrayLength } from '../index.ts'; -describe('checkArrayLength', () => { - it('throws on different Length', () => { - const expected = /x and y arrays must have the same length/; - expect(() => checkArrayLength([], [1])).toThrow(expected); - expect(() => checkArrayLength([1], [])).toThrow(expected); - expect(() => checkArrayLength([1], [1, 2])).toThrow(expected); - }); +test('throws on different Length', () => { + const expected = /x and y arrays must have the same length/; - it('throws if not arrays', () => { - const expected = /x and y must be arrays/; - // @ts-expect-error testing invalid input - expect(() => checkArrayLength(null, [1])).toThrow(expected); - // @ts-expect-error testing invalid input - expect(() => checkArrayLength([], null)).toThrow(expected); - // @ts-expect-error testing invalid input - expect(() => checkArrayLength()).toThrow(expected); - // @ts-expect-error testing invalid input - expect(() => checkArrayLength(42, [])).toThrow(expected); - // @ts-expect-error testing invalid input - expect(() => checkArrayLength([1, 2, 3, 4, 5], 'hello')).toThrow(expected); - }); + expect(() => checkArrayLength([], [1])).toThrow(expected); + expect(() => checkArrayLength([1], [])).toThrow(expected); + expect(() => checkArrayLength([1], [1, 2])).toThrow(expected); +}); + +test('throws if not arrays', () => { + const expected = /x and y must be arrays/; + + // @ts-expect-error testing invalid input + expect(() => checkArrayLength(null, [1])).toThrow(expected); + // @ts-expect-error testing invalid input + expect(() => checkArrayLength([], null)).toThrow(expected); + // @ts-expect-error testing invalid input + expect(() => checkArrayLength()).toThrow(expected); + // @ts-expect-error testing invalid input + expect(() => checkArrayLength(42, [])).toThrow(expected); + // @ts-expect-error testing invalid input + expect(() => checkArrayLength([1, 2, 3, 4, 5], 'hello')).toThrow(expected); +}); - it('correct result', () => { - expect(checkArrayLength([1, 2], [2, 3])).toBeUndefined(); - expect( - checkArrayLength(new Float64Array([1, 2]), new Float64Array([2, 3])), - ).toBeUndefined(); - expect(checkArrayLength([1, 2], new Float64Array([2, 3]))).toBeUndefined(); - }); +test('correct result', () => { + expect(checkArrayLength([1, 2], [2, 3])).toBeUndefined(); + expect( + checkArrayLength(new Float64Array([1, 2]), new Float64Array([2, 3])), + ).toBeUndefined(); + expect(checkArrayLength([1, 2], new Float64Array([2, 3]))).toBeUndefined(); }); diff --git a/src/__tests__/maybeToPrecision.test.ts b/src/__tests__/maybeToPrecision.test.ts index 1ca46d9..21bd310 100644 --- a/src/__tests__/maybeToPrecision.test.ts +++ b/src/__tests__/maybeToPrecision.test.ts @@ -1,36 +1,34 @@ -import { describe, expect, it } from 'vitest'; +import { expect, test } from 'vitest'; -import { maybeToPrecision } from '..'; +import { maybeToPrecision } from '../index.ts'; -describe('maybeToPrecision', () => { - it('positive number - no digit', () => { - expect(maybeToPrecision(0)).toBe('0'); - expect(maybeToPrecision(10)).toBe('10'); - expect(maybeToPrecision(0.052469)).toBe('0.052469'); - }); +test('positive number - no digit', () => { + expect(maybeToPrecision(0)).toBe('0'); + expect(maybeToPrecision(10)).toBe('10'); + expect(maybeToPrecision(0.052469)).toBe('0.052469'); +}); - it('positive number - digit', () => { - expect(maybeToPrecision(0, 1)).toBe('0'); - expect(maybeToPrecision(0, 2)).toBe('0.0'); - expect(maybeToPrecision(0.52469, 3)).toBe('0.525'); - }); +test('positive number - digit', () => { + expect(maybeToPrecision(0, 1)).toBe('0'); + expect(maybeToPrecision(0, 2)).toBe('0.0'); + expect(maybeToPrecision(0.52469, 3)).toBe('0.525'); +}); - it('negative number - no digit', () => { - expect(maybeToPrecision(-0)).toBe('0'); - expect(maybeToPrecision(-10)).toBe('- 10'); - expect(maybeToPrecision(-0.052469)).toBe('- 0.052469'); - }); +test('negative number - no digit', () => { + expect(maybeToPrecision(-0)).toBe('0'); + expect(maybeToPrecision(-10)).toBe('- 10'); + expect(maybeToPrecision(-0.052469)).toBe('- 0.052469'); +}); - it('negative number - digit', () => { - expect(maybeToPrecision(-0, 1)).toBe('0'); - expect(maybeToPrecision(-0, 2)).toBe('0.0'); - expect(maybeToPrecision(-0.52469, 3)).toBe('- 0.525'); - expect(maybeToPrecision(-4, 3)).toBe('- 4.00'); - }); +test('negative number - digit', () => { + expect(maybeToPrecision(-0, 1)).toBe('0'); + expect(maybeToPrecision(-0, 2)).toBe('0.0'); + expect(maybeToPrecision(-0.52469, 3)).toBe('- 0.525'); + expect(maybeToPrecision(-4, 3)).toBe('- 4.00'); +}); - it('wrong digit option', () => { - expect(() => { - maybeToPrecision(0, 0); - }).toThrow(/toPrecision\(\) argument must be between 1 and (?:100|21)/); - }); +test('wrong digit option', () => { + expect(() => { + maybeToPrecision(0, 0); + }).toThrow(/toPrecision\(\) argument must be between 1 and (?:100|21)/); }); diff --git a/src/checkArrayLength.ts b/src/checkArrayLength.ts index 84a1a11..caf6843 100644 --- a/src/checkArrayLength.ts +++ b/src/checkArrayLength.ts @@ -1,10 +1,12 @@ import type { NumberArray } from 'cheminfo-types'; import { isAnyArray } from 'is-any-array'; + /** * Check that x and y are arrays with the same length. * @param x - first array * @param y - second array - * @throws if x or y are not the same length, or if they are not arrays + * @throws {TypeError} if x or y are not arrays. + * @throws {RangeError} if x and y do not have the same length. */ export function checkArrayLength(x: NumberArray, y: NumberArray) { if (!isAnyArray(x) || !isAnyArray(y)) { diff --git a/src/index.ts b/src/index.ts index 0175194..509e0ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export * from './BaseRegression'; -export * from './checkArrayLength'; -export * from './maybeToPrecision'; +export * from './BaseRegression.ts'; +export * from './checkArrayLength.ts'; +export * from './maybeToPrecision.ts'; diff --git a/src/maybeToPrecision.ts b/src/maybeToPrecision.ts index 24afffc..3b1c902 100644 --- a/src/maybeToPrecision.ts +++ b/src/maybeToPrecision.ts @@ -1,8 +1,8 @@ /** * Cast `number` to string. Optionally `digits` specifies significant figures. - * @param number - * @param figures - * @returns - A string representation of `number`. + * @param number - The number to convert. + * @param figures - Number of significant figures. + * @returns A string representation of `number`. */ export function maybeToPrecision(number: number, figures?: number) { if (number < 0) { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..d55545e --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["**/__tests__", "**/*.test.*"] +} diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json deleted file mode 100644 index 9ea4847..0000000 --- a/tsconfig.cjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "module": "commonjs", - "declaration": true, - "declarationMap": true - }, - "exclude": [ - "./src/**/__tests__" - ] -} diff --git a/tsconfig.esm.json b/tsconfig.esm.json deleted file mode 100644 index 050b45d..0000000 --- a/tsconfig.esm.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.cjs.json", - "compilerOptions": { - "module": "es2020", - "outDir": "lib-esm" - } -} diff --git a/tsconfig.json b/tsconfig.json index 59aea42..5964c49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,9 @@ { + "extends": "@zakodium/tsconfig", "compilerOptions": { - "allowJs": true, - "esModuleInterop": true, - "moduleResolution": "node", + "noUncheckedIndexedAccess": true, "outDir": "lib", - "sourceMap": true, - "skipLibCheck": true, - "strict": true, - "target": "es2020", - "ignoreDeprecations": "6.0" + "types": ["node"] }, - "include": ["./src/**/*"] + "include": ["src", "vite*.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7ed5e86 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + snapshotFormat: { + maxOutputLength: Number.MAX_SAFE_INTEGER, + }, + }, +});