feat(middleware): add datapoint transform engine with round, scale, and map support
- src/modules/datapoints/transforms.ts: applyTransform() supporting map (priority), scale, and round - tests/transforms.test.ts: 17 tests covering no-transform passthrough, round precision, scale multiply, map lookup, map priority over numeric transforms, combined transforms, and edge cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f7d16c077e
commit
0df1c523bd
45
display/middleware/src/modules/datapoints/transforms.ts
Normal file
45
display/middleware/src/modules/datapoints/transforms.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export interface TransformConfig {
|
||||
round?: number;
|
||||
scale?: number;
|
||||
map?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a transformation pipeline to a raw string value.
|
||||
*
|
||||
* Processing order:
|
||||
* 1. If `map` is defined and the value exists as a key, return the mapped value immediately.
|
||||
* 2. Parse the value as a float. If it's not numeric, return as-is.
|
||||
* 3. Apply `scale` (multiply).
|
||||
* 4. Apply `round` (toFixed, then remove trailing zeros).
|
||||
*/
|
||||
export function applyTransform(value: string, config: TransformConfig | null): string {
|
||||
if (!config || Object.keys(config).length === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Map takes priority — if key matches, return immediately without numeric transforms
|
||||
if (config.map && Object.prototype.hasOwnProperty.call(config.map, value)) {
|
||||
return config.map[value];
|
||||
}
|
||||
|
||||
// Only apply numeric transforms if value is actually a number
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let result = num;
|
||||
|
||||
// Apply scale
|
||||
if (typeof config.scale === 'number') {
|
||||
result = result * config.scale;
|
||||
}
|
||||
|
||||
// Apply round
|
||||
if (typeof config.round === 'number') {
|
||||
result = parseFloat(result.toFixed(config.round));
|
||||
}
|
||||
|
||||
return String(result);
|
||||
}
|
||||
84
display/middleware/tests/transforms.test.ts
Normal file
84
display/middleware/tests/transforms.test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyTransform } from '../src/modules/datapoints/transforms';
|
||||
|
||||
describe('applyTransform', () => {
|
||||
describe('no transform', () => {
|
||||
it('should return value unchanged when config is null', () => {
|
||||
expect(applyTransform('42', null)).toBe('42');
|
||||
});
|
||||
|
||||
it('should return value unchanged when config is empty object', () => {
|
||||
expect(applyTransform('hello', {})).toBe('hello');
|
||||
});
|
||||
|
||||
it('should return non-numeric string unchanged with no transform', () => {
|
||||
expect(applyTransform('running', null)).toBe('running');
|
||||
});
|
||||
});
|
||||
|
||||
describe('round', () => {
|
||||
it('should round to specified decimal places', () => {
|
||||
expect(applyTransform('3.14159', { round: 2 })).toBe('3.14');
|
||||
});
|
||||
|
||||
it('should round to 0 decimal places', () => {
|
||||
expect(applyTransform('3.7', { round: 0 })).toBe('4');
|
||||
});
|
||||
|
||||
it('should round to integer by default when round is 0', () => {
|
||||
expect(applyTransform('2.5', { round: 0 })).toBe('3');
|
||||
});
|
||||
|
||||
it('should return non-numeric string unchanged when rounding', () => {
|
||||
expect(applyTransform('N/A', { round: 2 })).toBe('N/A');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scale', () => {
|
||||
it('should multiply value by scale factor', () => {
|
||||
expect(applyTransform('100', { scale: 0.1 })).toBe('10');
|
||||
});
|
||||
|
||||
it('should apply scale and round together', () => {
|
||||
expect(applyTransform('100', { scale: 0.333, round: 1 })).toBe('33.3');
|
||||
});
|
||||
|
||||
it('should return non-numeric string unchanged when scaling', () => {
|
||||
expect(applyTransform('unknown', { scale: 2 })).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('map', () => {
|
||||
it('should map matching value to its mapped result', () => {
|
||||
expect(applyTransform('1', { map: { '1': 'Online', '0': 'Offline' } })).toBe('Online');
|
||||
});
|
||||
|
||||
it('should map another matching value', () => {
|
||||
expect(applyTransform('0', { map: { '1': 'Online', '0': 'Offline' } })).toBe('Offline');
|
||||
});
|
||||
|
||||
it('should return original value when no map key matches', () => {
|
||||
expect(applyTransform('2', { map: { '1': 'Online', '0': 'Offline' } })).toBe('2');
|
||||
});
|
||||
|
||||
it('map should take priority over scale and round', () => {
|
||||
expect(
|
||||
applyTransform('1', { map: { '1': 'yes' }, scale: 10, round: 2 })
|
||||
).toBe('yes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('combined transforms', () => {
|
||||
it('should apply scale then round', () => {
|
||||
expect(applyTransform('1000', { scale: 0.001, round: 2 })).toBe('1');
|
||||
});
|
||||
|
||||
it('should handle negative scale', () => {
|
||||
expect(applyTransform('10', { scale: -1 })).toBe('-10');
|
||||
});
|
||||
|
||||
it('should handle large round precision', () => {
|
||||
expect(applyTransform('1.23456789', { round: 5 })).toBe('1.23457');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user