Skip to content

Commit

Permalink
add closest number node (#516)
Browse files Browse the repository at this point in the history
* add closest number node

* update to not use absolute
  • Loading branch information
mck authored Nov 12, 2024
1 parent d072e1c commit 18f41d3
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-dolls-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/graph-engine": minor
---

Add Closest Number node that allows you to get the closest number from an array of numbers.
85 changes: 85 additions & 0 deletions packages/graph-engine/src/nodes/math/closestNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { INodeDefinition, ToInput, ToOutput } from '../../index.js';
import { Node } from '../../programmatic/node.js';
import { NumberSchema } from '../../schemas/index.js';
import { arrayOf } from '../../schemas/utils.js';

export default class NodeDefinition extends Node {
static title = 'Closest Number';
static type = 'studio.tokens.math.closestNumber';
static description = 'Finds the closest number in an array to a target value';

declare inputs: ToInput<{
numbers: number[];
target: number;
}>;

declare outputs: ToOutput<{
index: number;
value: number;
difference: number;
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('numbers', {
type: {
...arrayOf(NumberSchema),
default: [1, 2, 3],
description: 'Array of numbers to search through'
}
});
this.addInput('target', {
type: {
...NumberSchema,
default: 2,
description: 'Target number to find closest match for'
}
});

this.addOutput('index', {
type: {
...NumberSchema,
description: 'Index of the closest number'
}
});
this.addOutput('value', {
type: {
...NumberSchema,
description: 'The closest number found'
}
});
this.addOutput('difference', {
type: {
...NumberSchema,
description: 'Absolute difference between target and closest number'
}
});
}

execute(): void | Promise<void> {
const { numbers, target } = this.getAllInputs();

if (!numbers || numbers.length === 0) {
throw new Error('Input array cannot be empty');
}

const result = numbers.reduce(
(acc, curr, idx) => {
const difference = target - curr;
if (Math.abs(difference) < Math.abs(acc.difference)) {
return { index: idx, value: curr, difference };
}
return acc;
},
{
index: 0,
value: numbers[0],
difference: target - numbers[0]
}
);

this.outputs.index.set(result.index);
this.outputs.value.set(result.value);
this.outputs.difference.set(result.difference);
}
}
2 changes: 2 additions & 0 deletions packages/graph-engine/src/nodes/math/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import add from './add.js';
import addVariadic from './addVariadic.js';
import ceil from './ceil.js';
import clamp from './clamp.js';
import closestNumber from './closestNumber.js';
import cos from './cos.js';
import count from './count.js';
import distance from './difference.js';
Expand Down Expand Up @@ -32,6 +33,7 @@ export const nodes = [
addVariadic,
ceil,
clamp,
closestNumber,
cos,
count,
rangeMapping,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Graph } from '../../../../src/graph/graph.js';
import { describe, expect, test } from 'vitest';
import Node from '../../../../src/nodes/math/closestNumber.js';

describe('math/closestNumber', () => {
test('finds exact match', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.numbers.setValue([1, 2, 3, 4, 5]);
node.inputs.target.setValue(3);

await node.execute();

expect(node.outputs.index.value).toBe(2);
expect(node.outputs.value.value).toBe(3);
expect(node.outputs.difference.value).toBe(0);
});

test('finds closest lower number', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.numbers.setValue([1, 2, 4, 5]);
node.inputs.target.setValue(3);

await node.execute();

expect(node.outputs.index.value).toBe(1);
expect(node.outputs.value.value).toBe(2);
expect(node.outputs.difference.value).toBe(1); // target (3) - value (2) = 1
});

test('finds closest higher number', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.numbers.setValue([1, 2, 4, 5]);
node.inputs.target.setValue(3.5);

await node.execute();

expect(node.outputs.index.value).toBe(2);
expect(node.outputs.value.value).toBe(4);
expect(node.outputs.difference.value).toBe(-0.5); // target (3.5) - value (4) = -0.5
});

test('handles negative numbers', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.numbers.setValue([-5, -2, 0, 2, 5]);
node.inputs.target.setValue(-1);

await node.execute();

expect(node.outputs.index.value).toBe(1);
expect(node.outputs.value.value).toBe(-2);
expect(node.outputs.difference.value).toBe(1); // target (-1) - value (-2) = 1
});

test('throws error for empty array', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.numbers.setValue([]);
node.inputs.target.setValue(1);

try {
await node.execute();
expect.fail('Should have thrown an error');
} catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message).to.equal('Input array cannot be empty');
}
});
});

0 comments on commit 18f41d3

Please sign in to comment.