Skip to content

Commit 9c9e5a1

Browse files
authored
feat: Implement zonesToXEquallySpaced (#356)
1 parent 33e68a1 commit 9c9e5a1

5 files changed

Lines changed: 147 additions & 0 deletions

File tree

src/__tests__/__snapshots__/index.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ exports[`existence of exported functions 1`] = `
148148
"xyObjectToXY",
149149
"zonesNormalize",
150150
"zonesWithPoints",
151+
"zonesToXEquallySpaced",
151152
"matrixAbsoluteMedian",
152153
"matrixApplyNumericalEncoding",
153154
"matrixAutoCorrelation",
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { zonesToXEquallySpaced } from '../zonesToXEquallySpaced.ts';
4+
5+
describe('zonesToXEquallySpaced', () => {
6+
it('should distribute points equally across a single zone', () => {
7+
const zones = [{ from: 0, to: 10 }];
8+
const result = zonesToXEquallySpaced(zones, 5);
9+
10+
expect(result).toHaveLength(5);
11+
// Edges are excluded: approx [1.6667, 3.3333, 5, 6.6667, 8.3333]
12+
expect(result[0]).toBeCloseTo(10 / 6, 5);
13+
expect(result[2]).toBeCloseTo(5, 5);
14+
expect(result[4]).toBeCloseTo((10 * 5) / 6, 5);
15+
});
16+
17+
it('should distribute points across multiple zones without repeating edges', () => {
18+
const zones = [
19+
{ from: 0, to: 5 },
20+
{ from: 10, to: 15 },
21+
];
22+
const result = zonesToXEquallySpaced(zones, 10);
23+
24+
expect(result).toHaveLength(10);
25+
// Edges excluded: first value ~0.8333, last value ~14.1667
26+
expect(result[0]).toBeCloseTo(5 / 6, 5);
27+
expect(result[9]).toBeCloseTo(15 - 5 / 6, 5);
28+
29+
// Check that zone boundary (5) is not in the result
30+
const has5 = Array.from(result).some((v) => Math.abs(v - 5) < 0.01);
31+
32+
expect(has5).toBe(false);
33+
});
34+
35+
it('should handle zones with custom from/to options', () => {
36+
const zones = [{ from: 2, to: 8 }];
37+
const result = zonesToXEquallySpaced(zones, 3, { from: 0, to: 10 });
38+
39+
expect(result).toHaveLength(3);
40+
// Edges excluded: values are approx [3.5, 5, 6.5]
41+
expect(result[0]).toBeCloseTo(3.5, 5);
42+
expect(result[1]).toBeCloseTo(5, 5);
43+
expect(result[2]).toBeCloseTo(6.5, 5);
44+
});
45+
46+
it('should throw error if zones array is empty', () => {
47+
expect(() => zonesToXEquallySpaced([], 10)).toThrow(
48+
'zones array must not be empty',
49+
);
50+
});
51+
52+
it('should throw error if numberOfPoints is less than 1', () => {
53+
const zones = [{ from: 0, to: 10 }];
54+
55+
expect(() => zonesToXEquallySpaced(zones, 0)).toThrow(
56+
"'numberOfPoints' must be greater than 0",
57+
);
58+
});
59+
60+
it('should throw error if from is greater than to', () => {
61+
const zones = [{ from: 10, to: 0 }];
62+
63+
expect(() => zonesToXEquallySpaced(zones, 10)).toThrow(
64+
'from should be less than or equal to to',
65+
);
66+
});
67+
});

src/zones/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './zonesNormalize.ts';
22
export * from './zonesWithPoints.ts';
3+
export * from './zonesToXEquallySpaced.ts';

src/zones/zonesToXEquallySpaced.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { FromTo } from 'cheminfo-types';
2+
3+
import { createFromToArray } from '../utils/createFromToArray.ts';
4+
5+
import { zonesNormalize } from './zonesNormalize.ts';
6+
import { zonesWithPoints } from './zonesWithPoints.ts';
7+
8+
export interface XZonesEquallySpacedOptions {
9+
/**
10+
* from - the minimum x value
11+
* @default zones[0].from
12+
*/
13+
from?: number;
14+
15+
/**
16+
* to - the maximum x value
17+
* @default zones[zones.length-1].to
18+
*/
19+
to?: number;
20+
}
21+
22+
/**
23+
* Function that returns a Number array of equally spaced numberOfPoints x values
24+
* distributed across the specified zones.
25+
* @param zones - array of from/to zones where x values should be distributed
26+
* @param numberOfPoints - total number of points to distribute across all zones
27+
* @param options - options
28+
* @returns array of equally spaced x values distributed across zones
29+
*/
30+
export function zonesToXEquallySpaced(
31+
zones: FromTo[],
32+
numberOfPoints: number,
33+
options: XZonesEquallySpacedOptions = {},
34+
): Float64Array {
35+
if (!zones || zones.length === 0) {
36+
throw new RangeError('zones array must not be empty');
37+
}
38+
39+
if (numberOfPoints < 1) {
40+
throw new RangeError("'numberOfPoints' must be greater than 0");
41+
}
42+
43+
const from = options.from ?? zones[0].from;
44+
//@ts-expect-error zones is tested before
45+
const to = options.to ?? zones.at(-1).to;
46+
47+
if (from > to) {
48+
throw new RangeError('from should be less than or equal to to');
49+
}
50+
51+
const normalizedZones = zonesNormalize(zones, { from, to, exclusions: [] });
52+
const zonesWithPointsRes = zonesWithPoints(normalizedZones, numberOfPoints, {
53+
from,
54+
to,
55+
}).filter((zone) => zone.numberOfPoints);
56+
57+
let xResult: number[] = [];
58+
for (const zone of zonesWithPointsRes) {
59+
if (!zone.numberOfPoints) {
60+
zone.numberOfPoints = 0;
61+
}
62+
63+
const zoneXValues = Array.from(
64+
createFromToArray({
65+
from: zone.from,
66+
to: zone.to,
67+
length: zone.numberOfPoints,
68+
includeFrom: false,
69+
includeTo: false,
70+
}),
71+
);
72+
73+
xResult = xResult.concat(zoneXValues);
74+
}
75+
76+
return new Float64Array(xResult);
77+
}

vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default defineConfig({
44
test: {
55
coverage: {
66
include: ['src/**'],
7+
exclude: ['**/__tests__/data/**'],
78
},
89
setupFiles: ['vitest.setup.ts'],
910
},

0 commit comments

Comments
 (0)