From 5c9f09c16e6997daeb913f60b79c11cda7b20e30 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:04:49 +0200 Subject: [PATCH 1/8] feat: add xyFromInterleaved and xyToInterleaved Convert between flat [x,y,x,y,...] arrays and DataXY objects. --- src/xy/__tests__/xyFromInterleaved.test.ts | 21 +++++++++++++++++++++ src/xy/__tests__/xyToInterleaved.test.ts | 20 ++++++++++++++++++++ src/xy/index.ts | 2 ++ src/xy/xyFromInterleaved.ts | 19 +++++++++++++++++++ src/xy/xyToInterleaved.ts | 19 +++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 src/xy/__tests__/xyFromInterleaved.test.ts create mode 100644 src/xy/__tests__/xyToInterleaved.test.ts create mode 100644 src/xy/xyFromInterleaved.ts create mode 100644 src/xy/xyToInterleaved.ts diff --git a/src/xy/__tests__/xyFromInterleaved.test.ts b/src/xy/__tests__/xyFromInterleaved.test.ts new file mode 100644 index 00000000..04f3381f --- /dev/null +++ b/src/xy/__tests__/xyFromInterleaved.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'vitest'; + +import { xyFromInterleaved } from '../index.ts'; + +test('basic', () => { + expect(xyFromInterleaved([1, 10, 2, 20, 3, 30])).toStrictEqual({ + x: [1, 2, 3], + y: [10, 20, 30], + }); +}); + +test('Float64Array input', () => { + expect(xyFromInterleaved(new Float64Array([1, 10, 2, 20]))).toStrictEqual({ + x: [1, 2], + y: [10, 20], + }); +}); + +test('empty array', () => { + expect(xyFromInterleaved([])).toStrictEqual({ x: [], y: [] }); +}); diff --git a/src/xy/__tests__/xyToInterleaved.test.ts b/src/xy/__tests__/xyToInterleaved.test.ts new file mode 100644 index 00000000..669a5a4d --- /dev/null +++ b/src/xy/__tests__/xyToInterleaved.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest'; + +import { xyFromInterleaved, xyToInterleaved } from '../index.ts'; + +test('basic', () => { + expect(xyToInterleaved({ x: [1, 2, 3], y: [10, 20, 30] })).toStrictEqual([ + 1, 10, 2, 20, 3, 30, + ]); +}); + +test('empty', () => { + expect(xyToInterleaved({ x: [], y: [] })).toStrictEqual([]); +}); + +test('round-trip with xyFromInterleaved', () => { + const interleaved = [1, 10, 2, 20, 3, 30]; + expect(xyToInterleaved(xyFromInterleaved(interleaved))).toStrictEqual( + interleaved, + ); +}); diff --git a/src/xy/index.ts b/src/xy/index.ts index a8a9b914..472429d3 100644 --- a/src/xy/index.ts +++ b/src/xy/index.ts @@ -7,6 +7,7 @@ export * from './xyEnsureGrowingX.ts'; export * from './xyEquallySpaced.ts'; export * from './xyExtract.ts'; export * from './xyFilter.ts'; +export * from './xyFromInterleaved.ts'; export * from './xyFilterMinYValue.ts'; export * from './xyFilterTopYValues.ts'; export * from './xyFilterX.ts'; @@ -41,6 +42,7 @@ export * from './xyRolling.ts'; export * from './xyRollingCircleTransform.ts'; export * from './xySetYValue.ts'; export * from './xySortX.ts'; +export * from './xyToInterleaved.ts'; export * from './xyToXYArray.ts'; export * from './xyToXYObject.ts'; export * from './xyUniqueX.ts'; diff --git a/src/xy/xyFromInterleaved.ts b/src/xy/xyFromInterleaved.ts new file mode 100644 index 00000000..3326ef19 --- /dev/null +++ b/src/xy/xyFromInterleaved.ts @@ -0,0 +1,19 @@ +import type { DataXY } from 'cheminfo-types'; + +/** + * Convert a flat interleaved array [x, y, x, y, ...] to a DataXY object. + * @param data - Flat array alternating x and y values. + * @returns DataXY object with separate x and y arrays. + */ +export function xyFromInterleaved( + data: number[] | Float64Array, +): DataXY { + const length = data.length / 2; + const x: number[] = new Array(length); + const y: number[] = new Array(length); + for (let i = 0; i < length; i++) { + x[i] = data[2 * i]; + y[i] = data[2 * i + 1]; + } + return { x, y }; +} diff --git a/src/xy/xyToInterleaved.ts b/src/xy/xyToInterleaved.ts new file mode 100644 index 00000000..6a3c42b5 --- /dev/null +++ b/src/xy/xyToInterleaved.ts @@ -0,0 +1,19 @@ +import type { DataXY } from 'cheminfo-types'; + +import { xyCheck } from './xyCheck.ts'; + +/** + * Convert a DataXY object to a flat interleaved array [x, y, x, y, ...]. + * @param data - DataXY object with x and y arrays. + * @returns Flat array alternating x and y values. + */ +export function xyToInterleaved(data: DataXY): number[] { + xyCheck(data); + const { x, y } = data; + const result: number[] = new Array(x.length * 2); + for (let i = 0; i < x.length; i++) { + result[2 * i] = x[i]; + result[2 * i + 1] = y[i]; + } + return result; +} From 75928a2802c6be32a93e9d419c1da44812f3c9d7 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:10:53 +0200 Subject: [PATCH 2/8] chore: update exports snapshot and add .claude to .gitignore --- .gitignore | 1 + src/__tests__/__snapshots__/index.test.ts.snap | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7dea02f3..bf34965b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ dist .DS_Store .eslintcache +.claude diff --git a/src/__tests__/__snapshots__/index.test.ts.snap b/src/__tests__/__snapshots__/index.test.ts.snap index 0107959e..f87c96fe 100644 --- a/src/__tests__/__snapshots__/index.test.ts.snap +++ b/src/__tests__/__snapshots__/index.test.ts.snap @@ -91,6 +91,7 @@ exports[`existence of exported functions 1`] = ` "xyEquallySpaced", "xyExtract", "xyFilter", + "xyFromInterleaved", "xyFilterMinYValue", "xyFilterTopYValues", "xyFilterX", @@ -124,6 +125,7 @@ exports[`existence of exported functions 1`] = ` "xyRollingCircleTransform", "xySetYValue", "xySortX", + "xyToInterleaved", "xyToXYArray", "xyToXYObject", "xyUniqueX", From ba52dae5595bbdc73bc50232ffd57125460be9e5 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:13:32 +0200 Subject: [PATCH 3/8] chore: fix eslint --- src/xy/__tests__/xyToInterleaved.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xy/__tests__/xyToInterleaved.test.ts b/src/xy/__tests__/xyToInterleaved.test.ts index 669a5a4d..9bded07f 100644 --- a/src/xy/__tests__/xyToInterleaved.test.ts +++ b/src/xy/__tests__/xyToInterleaved.test.ts @@ -14,6 +14,7 @@ test('empty', () => { test('round-trip with xyFromInterleaved', () => { const interleaved = [1, 10, 2, 20, 3, 30]; + expect(xyToInterleaved(xyFromInterleaved(interleaved))).toStrictEqual( interleaved, ); From 4692d6b76f39541d3163f04463037ed2807d905c Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:18:39 +0200 Subject: [PATCH 4/8] chore: fix eslint --- src/matrix/matrixPQN.ts | 2 +- src/utils/__tests__/calculateAdaptiveWeights.test.ts | 7 +------ src/xy/xyFilterX.ts | 2 +- src/xy/xyReduce.ts | 2 +- src/xy/xyReduceNonContinuous.ts | 6 +----- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/matrix/matrixPQN.ts b/src/matrix/matrixPQN.ts index f72cfa20..c1094ab3 100644 --- a/src/matrix/matrixPQN.ts +++ b/src/matrix/matrixPQN.ts @@ -32,7 +32,7 @@ export function matrixPQN( medianOfQuotients: number[]; } { const { max = 100 } = options; - const matrixB = new Matrix(matrix as number[][]); + const matrixB = new Matrix(matrix); for (let i = 0; i < matrixB.rows; i++) { const normalizationFactor = matrixB.getRowVector(i).norm('frobenius') / max; const row = matrixB.getRowVector(i).div(normalizationFactor); diff --git a/src/utils/__tests__/calculateAdaptiveWeights.test.ts b/src/utils/__tests__/calculateAdaptiveWeights.test.ts index 14c8fe2f..ca68c153 100644 --- a/src/utils/__tests__/calculateAdaptiveWeights.test.ts +++ b/src/utils/__tests__/calculateAdaptiveWeights.test.ts @@ -134,12 +134,7 @@ test('works with different array types', () => { const baseline = [1.1, 2.1, 3.1, 4.1, 5.1]; const weights = [1, 1, 1, 1, 1]; - const result = calculateAdaptiveWeights( - yData as any, - baseline as any, - weights as any, - {}, - ); + const result = calculateAdaptiveWeights(yData, baseline, weights, {}); expect(result).toBeDefined(); expect(result[0]).toBe(1); diff --git a/src/xy/xyFilterX.ts b/src/xy/xyFilterX.ts index aad0d0a9..c1b7a13a 100644 --- a/src/xy/xyFilterX.ts +++ b/src/xy/xyFilterX.ts @@ -47,7 +47,7 @@ export function xyFilterX( } const { from = x[0], - to = x.at(-1) as number, + to = x.at(-1), zones = [{ from, to }], exclusions = [], } = options; diff --git a/src/xy/xyReduce.ts b/src/xy/xyReduce.ts index c370786c..2638a96a 100644 --- a/src/xy/xyReduce.ts +++ b/src/xy/xyReduce.ts @@ -69,7 +69,7 @@ export function xyReduce( const { x, y } = data; const { from = x[0], - to = x.at(-1) as number, + to = x.at(-1), nbPoints = 4001, optimize = false, } = options; diff --git a/src/xy/xyReduceNonContinuous.ts b/src/xy/xyReduceNonContinuous.ts index dca47b59..d8b6c137 100644 --- a/src/xy/xyReduceNonContinuous.ts +++ b/src/xy/xyReduceNonContinuous.ts @@ -51,11 +51,7 @@ export function xyReduceNonContinuous( }; } const { x, y } = data; - const { - from = x[0], - to = x.at(-1) as number, - maxApproximateNbPoints = 4001, - } = options; + const { from = x[0], to = x.at(-1), maxApproximateNbPoints = 4001 } = options; let { zones = [] } = options; zones = zonesNormalize(zones, { from, to }); From 3315bc7cc4731da02df5ec82ba28f24687192fc5 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:27:46 +0200 Subject: [PATCH 5/8] fix: suppress TS errors where x.at(-1) is guaranteed non-empty --- src/xy/xyFilterX.ts | 1 + src/xy/xyReduce.ts | 1 + src/xy/xyReduceNonContinuous.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/xy/xyFilterX.ts b/src/xy/xyFilterX.ts index c1b7a13a..1fecd9ec 100644 --- a/src/xy/xyFilterX.ts +++ b/src/xy/xyFilterX.ts @@ -52,6 +52,7 @@ export function xyFilterX( exclusions = [], } = options; + // @ts-expect-error -- x.at(-1) returns number | undefined but array is guaranteed non-empty here const normalizedZones = zonesNormalize(zones, { from, to, exclusions }); let currentZoneIndex = 0; diff --git a/src/xy/xyReduce.ts b/src/xy/xyReduce.ts index 2638a96a..9e219adc 100644 --- a/src/xy/xyReduce.ts +++ b/src/xy/xyReduce.ts @@ -76,6 +76,7 @@ export function xyReduce( let { zones = [] } = options; zones = zonesNormalize(zones, { from, to }); + // @ts-expect-error -- x.at(-1) returns number | undefined but array is guaranteed non-empty here if (zones.length === 0) zones = [{ from, to }]; // we take everything const { internalZones, totalPoints } = getInternalZones(zones, x); diff --git a/src/xy/xyReduceNonContinuous.ts b/src/xy/xyReduceNonContinuous.ts index d8b6c137..decb38bb 100644 --- a/src/xy/xyReduceNonContinuous.ts +++ b/src/xy/xyReduceNonContinuous.ts @@ -55,6 +55,7 @@ export function xyReduceNonContinuous( let { zones = [] } = options; zones = zonesNormalize(zones, { from, to }); + // @ts-expect-error -- x.at(-1) returns number | undefined but array is guaranteed non-empty here if (zones.length === 0) zones = [{ from, to }]; // we take everything const { internalZones, totalPoints } = getInternalZones(zones, x); @@ -64,6 +65,7 @@ export function xyReduceNonContinuous( return notEnoughPoints(x, y, internalZones, totalPoints); } + // @ts-expect-error -- x.at(-1) returns number | undefined but array is guaranteed non-empty here const deltaX = (to - from) / (maxApproximateNbPoints - 1); const newX: number[] = []; const newY: number[] = []; From 775bbecd8a55db472f7223152928cf22fad5446f Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:33:48 +0200 Subject: [PATCH 6/8] feat: return Float64Array from xyFromInterleaved and xyToInterleaved --- src/xy/__tests__/xyFromInterleaved.test.ts | 13 ++++++++----- src/xy/__tests__/xyToInterleaved.test.ts | 11 +++++------ src/xy/xyFromInterleaved.ts | 6 +++--- src/xy/xyToInterleaved.ts | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/xy/__tests__/xyFromInterleaved.test.ts b/src/xy/__tests__/xyFromInterleaved.test.ts index 04f3381f..763dd36d 100644 --- a/src/xy/__tests__/xyFromInterleaved.test.ts +++ b/src/xy/__tests__/xyFromInterleaved.test.ts @@ -4,18 +4,21 @@ import { xyFromInterleaved } from '../index.ts'; test('basic', () => { expect(xyFromInterleaved([1, 10, 2, 20, 3, 30])).toStrictEqual({ - x: [1, 2, 3], - y: [10, 20, 30], + x: new Float64Array([1, 2, 3]), + y: new Float64Array([10, 20, 30]), }); }); test('Float64Array input', () => { expect(xyFromInterleaved(new Float64Array([1, 10, 2, 20]))).toStrictEqual({ - x: [1, 2], - y: [10, 20], + x: new Float64Array([1, 2]), + y: new Float64Array([10, 20]), }); }); test('empty array', () => { - expect(xyFromInterleaved([])).toStrictEqual({ x: [], y: [] }); + expect(xyFromInterleaved([])).toStrictEqual({ + x: new Float64Array(0), + y: new Float64Array(0), + }); }); diff --git a/src/xy/__tests__/xyToInterleaved.test.ts b/src/xy/__tests__/xyToInterleaved.test.ts index 9bded07f..a38f85eb 100644 --- a/src/xy/__tests__/xyToInterleaved.test.ts +++ b/src/xy/__tests__/xyToInterleaved.test.ts @@ -3,18 +3,17 @@ import { expect, test } from 'vitest'; import { xyFromInterleaved, xyToInterleaved } from '../index.ts'; test('basic', () => { - expect(xyToInterleaved({ x: [1, 2, 3], y: [10, 20, 30] })).toStrictEqual([ - 1, 10, 2, 20, 3, 30, - ]); + expect(xyToInterleaved({ x: [1, 2, 3], y: [10, 20, 30] })).toStrictEqual( + new Float64Array([1, 10, 2, 20, 3, 30]), + ); }); test('empty', () => { - expect(xyToInterleaved({ x: [], y: [] })).toStrictEqual([]); + expect(xyToInterleaved({ x: [], y: [] })).toStrictEqual(new Float64Array(0)); }); test('round-trip with xyFromInterleaved', () => { - const interleaved = [1, 10, 2, 20, 3, 30]; - + const interleaved = new Float64Array([1, 10, 2, 20, 3, 30]); expect(xyToInterleaved(xyFromInterleaved(interleaved))).toStrictEqual( interleaved, ); diff --git a/src/xy/xyFromInterleaved.ts b/src/xy/xyFromInterleaved.ts index 3326ef19..7ac6d55e 100644 --- a/src/xy/xyFromInterleaved.ts +++ b/src/xy/xyFromInterleaved.ts @@ -7,10 +7,10 @@ import type { DataXY } from 'cheminfo-types'; */ export function xyFromInterleaved( data: number[] | Float64Array, -): DataXY { +): DataXY { const length = data.length / 2; - const x: number[] = new Array(length); - const y: number[] = new Array(length); + const x = new Float64Array(length); + const y = new Float64Array(length); for (let i = 0; i < length; i++) { x[i] = data[2 * i]; y[i] = data[2 * i + 1]; diff --git a/src/xy/xyToInterleaved.ts b/src/xy/xyToInterleaved.ts index 6a3c42b5..30330d79 100644 --- a/src/xy/xyToInterleaved.ts +++ b/src/xy/xyToInterleaved.ts @@ -7,10 +7,10 @@ import { xyCheck } from './xyCheck.ts'; * @param data - DataXY object with x and y arrays. * @returns Flat array alternating x and y values. */ -export function xyToInterleaved(data: DataXY): number[] { +export function xyToInterleaved(data: DataXY): Float64Array { xyCheck(data); const { x, y } = data; - const result: number[] = new Array(x.length * 2); + const result = new Float64Array(x.length * 2); for (let i = 0; i < x.length; i++) { result[2 * i] = x[i]; result[2 * i + 1] = y[i]; From 80a36952ad68c35922b699f824b23cc93f74802b Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 09:40:42 +0200 Subject: [PATCH 7/8] chore: fix eslint --- src/xy/__tests__/xyToInterleaved.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xy/__tests__/xyToInterleaved.test.ts b/src/xy/__tests__/xyToInterleaved.test.ts index a38f85eb..37147502 100644 --- a/src/xy/__tests__/xyToInterleaved.test.ts +++ b/src/xy/__tests__/xyToInterleaved.test.ts @@ -14,6 +14,7 @@ test('empty', () => { test('round-trip with xyFromInterleaved', () => { const interleaved = new Float64Array([1, 10, 2, 20, 3, 30]); + expect(xyToInterleaved(xyFromInterleaved(interleaved))).toStrictEqual( interleaved, ); From 210820b9b69f57eab76a0949173aae0282583be7 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Tue, 12 May 2026 10:47:46 +0200 Subject: [PATCH 8/8] feat: throw RangeError when interleaved data length is odd --- src/xy/__tests__/xyFromInterleaved.test.ts | 6 ++++++ src/xy/xyFromInterleaved.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/xy/__tests__/xyFromInterleaved.test.ts b/src/xy/__tests__/xyFromInterleaved.test.ts index 763dd36d..2ed0f5dc 100644 --- a/src/xy/__tests__/xyFromInterleaved.test.ts +++ b/src/xy/__tests__/xyFromInterleaved.test.ts @@ -22,3 +22,9 @@ test('empty array', () => { y: new Float64Array(0), }); }); + +test('odd length throws', () => { + expect(() => xyFromInterleaved([1, 2, 3])).toThrow( + /data length must be even/, + ); +}); diff --git a/src/xy/xyFromInterleaved.ts b/src/xy/xyFromInterleaved.ts index 7ac6d55e..63d11068 100644 --- a/src/xy/xyFromInterleaved.ts +++ b/src/xy/xyFromInterleaved.ts @@ -8,6 +8,11 @@ import type { DataXY } from 'cheminfo-types'; export function xyFromInterleaved( data: number[] | Float64Array, ): DataXY { + if (data.length % 2 !== 0) { + throw new RangeError( + `xyFromInterleaved: data length must be even, got ${data.length}`, + ); + } const length = data.length / 2; const x = new Float64Array(length); const y = new Float64Array(length);