diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts new file mode 100644 index 0000000..28f7408 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts @@ -0,0 +1,31 @@ +import {CsvDirectoryReader} from '../../../../../lib/generic-cpu/helpers/csv-directory-reader'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('init: ', () => { + it('valid', () => { + const csvDirectoryReader: CsvDirectoryReader = new CsvDirectoryReader( + __dirname + '/test-data/' + ); + const expected1 = { + simple1: [ + ['CPU_perc', 'Throughput_reqPerSec', 'POWER_W'], + ['0', '0', '70'], + ['10', '600000', '90'], + ['20', '800000', '100'], + ], + }; + const expected2 = { + simple2: [ + ['CPU_perc', 'Throughput_reqPerSec', 'POWER_W'], + ['0', '0', '90'], + ['10', '700000', '100'], + ['20', '900000', '110'], + ], + }; + expect(csvDirectoryReader.read(['simple1'])).toEqual(expected1); + expect(csvDirectoryReader.read(['simple2'])).toEqual(expected2); + }); + }); + }); +}); diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts new file mode 100644 index 0000000..15a701a --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts @@ -0,0 +1,61 @@ +import {LinearInterpolator} from '../../../../../lib/generic-cpu/helpers/linear-interpolator'; + +describe('lib/generic-cpu/helpers:', () => { + describe('LinearInterpolator', () => { + describe('sanity: ', () => { + const linearnterpolator: LinearInterpolator = new LinearInterpolator(); + const lookupSeries = [10, 20, 40, 100]; + const valueSeries = [1000, 2000, 4000, 10000]; + const invalidLookupSeries = [10, -20, 40, 100]; + const invalidValueSeries = [1000, 2000, -4000, 10000]; + it('out of range low', () => { + expect( + linearnterpolator.interpolate(5, lookupSeries, valueSeries) + ).toEqual(500); + }); + it('out of range high', () => { + expect( + linearnterpolator.interpolate(150, lookupSeries, valueSeries) + ).toEqual(10000); + }); + it('no interpolation needed', () => { + expect( + linearnterpolator.interpolate(40, lookupSeries, valueSeries) + ).toEqual(4000); + }); + it('interpolation', () => { + expect( + linearnterpolator.interpolate(50, lookupSeries, valueSeries) + ).toEqual(5000); + }); + it('interpolation with percision', () => { + expect( + linearnterpolator.interpolate(20.01, lookupSeries, valueSeries) + ).toBeCloseTo(2001); + }); + it('negative lookup key', () => { + const interpolateCall = () => + linearnterpolator.interpolate(-20.01, lookupSeries, valueSeries); + expect(interpolateCall).toThrow(Error); + }); + it('negative item in lookup series', () => { + const interpolateCall = () => + linearnterpolator.interpolate( + 20.01, + invalidLookupSeries, + valueSeries + ); + expect(interpolateCall).toThrow(Error); + }); + it('negative item in value series', () => { + const interpolateCall = () => + linearnterpolator.interpolate( + 20.01, + lookupSeries, + invalidValueSeries + ); + expect(interpolateCall).toThrow(Error); + }); + }); + }); +}); diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts new file mode 100644 index 0000000..462852d --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts @@ -0,0 +1,35 @@ +import {CsvDirectoryReader} from '../../../../../lib/generic-cpu/helpers/csv-directory-reader'; +import {LinearInterpolator} from '../../../../../lib/generic-cpu/helpers/linear-interpolator'; +import {PowerCalculator} from '../../../../../lib/generic-cpu/helpers/power-calculator'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('sanity: ', () => { + const powerCalculator: PowerCalculator = new PowerCalculator( + ['prod1', 'prod2'], + new CsvDirectoryReader(__dirname + '/test-data/'), + new LinearInterpolator() + ); + it('calculate', () => { + expect(powerCalculator.calculate(15, 'prod1')).toEqual(95); + }); + it('simulate', () => { + expect(powerCalculator.simulate(15, 'prod1', 'prod2')).toEqual(110); + }); + it('calculate processor not found', () => { + const calculateCall = () => powerCalculator.calculate(15, 'prodX'); + expect(calculateCall).toThrow(Error); + }); + it('simulate physical processor not found', () => { + const simulateCall = () => + powerCalculator.simulate(15, 'prodX', 'prod2'); + expect(simulateCall).toThrow(Error); + }); + it('simulate simualted processor not found', () => { + const simulateCall = () => + powerCalculator.simulate(15, 'prod1', 'prodX'); + expect(simulateCall).toThrow(Error); + }); + }); + }); +}); diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv new file mode 100644 index 0000000..726db98 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv @@ -0,0 +1,13 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,800000,100 +30,1100000,120 +40,1400000,130 +50,1800000,150 +60,2200000,170 +70,2500000,180 +80,2600000,180 +80,2700000,180 +90,2750000,180 +100,2800000,180 \ No newline at end of file diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv new file mode 100644 index 0000000..98451ec --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv @@ -0,0 +1,13 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,700000,110 +30,1200000,130 +40,1500000,140 +50,1900000,170 +60,2300000,180 +70,2600000,190 +80,2700000,200 +80,2800000,210 +90,2850000,220 +100,2900000,220 \ No newline at end of file diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple1.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple1.csv new file mode 100644 index 0000000..f546909 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple1.csv @@ -0,0 +1,4 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,800000,100 \ No newline at end of file diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple2.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple2.csv new file mode 100644 index 0000000..ce42586 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple2.csv @@ -0,0 +1,4 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,90 +10,700000,100 +20,900000,110 \ No newline at end of file diff --git a/src/__tests__/unit/lib/generic-cpu/index.test.ts b/src/__tests__/unit/lib/generic-cpu/index.test.ts new file mode 100644 index 0000000..8c70922 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/index.test.ts @@ -0,0 +1,149 @@ +import {GenericCPU} from '../../../../lib'; +import {PluginParams} from '../../../../types/common'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('sanity: ', () => { + it('calculate-no-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 30, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 90, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, // => 100 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.000027777777777777776, // (100 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 30, // => 120 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.000033333333333333335, // (120 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 90, // => 180 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.00005, // (120 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + + it('calculate-with-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 15, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 24, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 61, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 15, // => 95 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.00002638888888888889, // (95 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 24, // => 108 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.000029999999999999997, // (108 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 61, // => 171 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.0000475, // (171 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + it('simulate-with-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + 'simulated-processor': 'prod2', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 55, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 61, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, // => 800000 req/sec => 22% => 114 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.000031666666666666666, // (114 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 55, // => 2000000 req/sec => 52.5% => 172.5 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.00004791666666666667, // (172.5 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 61, // => 2230000 req/sec => 58.25% => 178.25 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.00004951388888888889, // (178.25 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + }); + }); +}); diff --git a/src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts b/src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts new file mode 100644 index 0000000..c4e2775 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts @@ -0,0 +1,53 @@ +import {DataTable} from '../../../../../lib/generic-cpu/models/data-table'; + +describe('lib/generic-cpu/models:', () => { + describe('DataTable', () => { + describe('init: ', () => { + it('valid input data.', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '89', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const dataTable = new DataTable(inputData); + expect(dataTable.getSize()).toEqual(3); + expect(dataTable.getColumnData('id')).toEqual([ + 6565464, 1230985, 1237899, + ]); + expect(dataTable.getColumnData('grade')).toEqual([89, 91, 100]); + expect(dataTable.getColumnData('age')).toEqual([12, 11, 12]); + }); + it('missing header.', () => { + const inputData = [ + ['id', '', 'age'], + ['6565464', '89', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + it('missing value.', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + it('invalid value (NaN).', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '89', '12'], + ['1230985', 'qewrwrt', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + }); + }); +}); diff --git a/src/lib/generic-cpu/helpers/csv-directory-reader.ts b/src/lib/generic-cpu/helpers/csv-directory-reader.ts new file mode 100644 index 0000000..1fc5fda --- /dev/null +++ b/src/lib/generic-cpu/helpers/csv-directory-reader.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export class CsvDirectoryReader { + private directoryPath: string; + + constructor(directoryPath: string) { + this.validateDirectoryPath(directoryPath); + this.directoryPath = directoryPath; + } + + read(fileNames: string[]): {[key: string]: string[][]} { + const filesMapping: {[key: string]: string[][]} = {}; + for (const fileName of fileNames) { + const filePath = path.join(this.directoryPath, fileName + '.csv'); + const fileContent = fs.readFileSync(filePath, 'utf8'); + filesMapping[fileName] = this.parseCSV(fileContent); + } + return filesMapping; + } + + private validateDirectoryPath(directoryPath: string) { + if ( + !fs.existsSync(directoryPath) || + !fs.lstatSync(directoryPath).isDirectory() + ) { + throw new Error('Invalid directory path: ' + directoryPath); + } + } + + private parseCSV(csvString: string): string[][] { + const rows: string[] = csvString.split('\n'); + const result: string[][] = []; + for (const row of rows) { + const columns: string[] = row.split(','); + result.push(columns); + } + return result; + } +} diff --git a/src/lib/generic-cpu/helpers/linear-interpolator.ts b/src/lib/generic-cpu/helpers/linear-interpolator.ts new file mode 100644 index 0000000..f663657 --- /dev/null +++ b/src/lib/generic-cpu/helpers/linear-interpolator.ts @@ -0,0 +1,59 @@ +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; + +export class LinearInterpolator { + interpolate( + lookupKey: number, + lookupSeries: number[], + valueSeries: number[] + ): number { + this.validatePositiveNumbers([lookupKey], 'lookupKey'); + this.validatePositiveNumbers(lookupSeries, 'lookupSeries'); + this.validatePositiveNumbers(valueSeries, 'valueSeries'); + let x0: number; + let x1: number; + let y0: number; + let y1: number; + if (lookupKey > lookupSeries[lookupSeries.length - 1]) { + return valueSeries[valueSeries.length - 1]; + } else if (lookupKey < lookupSeries[0]) { + x0 = 0; + x1 = lookupSeries[0]; + y0 = 0; + y1 = valueSeries[0]; + } else { + const [leftIndex, rightIndex] = this.findBoundaryIndexes( + lookupKey, + lookupSeries + ); + x0 = lookupSeries[leftIndex]; + x1 = lookupSeries[rightIndex]; + y0 = valueSeries[leftIndex]; + y1 = valueSeries[rightIndex]; + } + const interpolatedValue = y0 + (lookupKey - x0) * ((y1 - y0) / (x1 - x0)); + return interpolatedValue; + } + + private findBoundaryIndexes( + lookupValue: number, + lookupSeries: number[] + ): [number, number] { + for (let i = 0; i < lookupSeries.length - 1; i++) { + if (lookupValue >= lookupSeries[i] && lookupValue < lookupSeries[i + 1]) { + return [i, i + 1]; + } + } + throw new InputValidationError( + `Failed to find boundary indexes for ${lookupValue} in ${lookupSeries}` + ); + } + + private validatePositiveNumbers(numbersArray: number[], arrayName: string) { + if (numbersArray.some(num => num < 0)) { + throw new InputValidationError( + `${arrayName} contains negative value(s): ${numbersArray}` + ); + } + } +} diff --git a/src/lib/generic-cpu/helpers/power-calculator.ts b/src/lib/generic-cpu/helpers/power-calculator.ts new file mode 100644 index 0000000..0a20acf --- /dev/null +++ b/src/lib/generic-cpu/helpers/power-calculator.ts @@ -0,0 +1,116 @@ +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; + +import {DataTable} from '../models/data-table'; +import {CsvDirectoryReader} from './csv-directory-reader'; +import {LinearInterpolator} from './linear-interpolator'; + +export class PowerCalculator { + private CPU_UTIL_COL_NAME = 'CPU_perc'; + private THROUGHPUT_COL_NAME = 'Throughput_reqPerSec'; + private POWER_COL_NAME = 'POWER_W'; + private prodNameToDataTable: Map = new Map< + string, + DataTable + >(); + private linearInterpolator: LinearInterpolator; + constructor( + productNames: string[], + csvDirectortyReader: CsvDirectoryReader, + linearInterpolator: LinearInterpolator + ) { + this.prodNameToDataTable = this.getProdNameToDataTable( + productNames, + csvDirectortyReader + ); + this.linearInterpolator = linearInterpolator; + } + + calculate(cpuUtil: number, processorName: string) { + const powerCurveDataTable = + this.getPowerCurveDataTableOrThrowError(processorName); + const cpuUtilSeries = powerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const powerSeries = powerCurveDataTable.getColumnData(this.POWER_COL_NAME); + const power = this.linearInterpolator.interpolate( + cpuUtil, + cpuUtilSeries, + powerSeries + ); + return power; + } + + simulate( + physicalCpuUtil: number, + physicalProcessorName: string, + simulatedProcessorName: string + ) { + const physicalPowerCurveDataTable = this.getPowerCurveDataTableOrThrowError( + physicalProcessorName + ); + const simulatedPowerCurveDataTable = + this.getPowerCurveDataTableOrThrowError(simulatedProcessorName); + // interpolate TP + const physicalThrouputSeries = physicalPowerCurveDataTable.getColumnData( + this.THROUGHPUT_COL_NAME + ); + const physicalCpuUtilSeries = physicalPowerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const throughput = this.linearInterpolator.interpolate( + physicalCpuUtil, + physicalCpuUtilSeries, + physicalThrouputSeries + ); + // interpolate simulated CPU util + const simulatedThroughputSeries = + simulatedPowerCurveDataTable.getColumnData(this.THROUGHPUT_COL_NAME); + const simulatedCpuUtilSeries = simulatedPowerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const simulatedCpuUtil = this.linearInterpolator.interpolate( + throughput, + simulatedThroughputSeries, + simulatedCpuUtilSeries + ); + // interpolate simulated power + const simulatedPowerSeries = simulatedPowerCurveDataTable.getColumnData( + this.POWER_COL_NAME + ); + const power = this.linearInterpolator.interpolate( + simulatedCpuUtil, + simulatedCpuUtilSeries, + simulatedPowerSeries + ); + return power; + } + + private getProdNameToDataTable( + productNames: string[], + csvDirectortyReader: CsvDirectoryReader + ): Map { + const prodNameToDataTable: Map = new Map< + string, + DataTable + >(); + const prodNameToCsvContent = csvDirectortyReader.read(productNames); + for (const key in prodNameToCsvContent) { + if (Object.prototype.hasOwnProperty.call(prodNameToCsvContent, key)) { + const content: string[][] = prodNameToCsvContent[key]; + prodNameToDataTable.set(key, new DataTable(content)); + } + } + return prodNameToDataTable; + } + + private getPowerCurveDataTableOrThrowError(prodName: string): DataTable { + const powerCurveDataTable = this.prodNameToDataTable.get(prodName); + if (powerCurveDataTable === undefined) { + throw new InputValidationError( + `no power curve available for ${prodName}` + ); + } + return powerCurveDataTable; + } +} diff --git a/src/lib/generic-cpu/index.ts b/src/lib/generic-cpu/index.ts new file mode 100644 index 0000000..ce165e9 --- /dev/null +++ b/src/lib/generic-cpu/index.ts @@ -0,0 +1,58 @@ +import {PluginInterface} from '../../interfaces'; +import {ConfigParams, PluginParams} from '../../types/common'; +import {CsvDirectoryReader} from './helpers/csv-directory-reader'; +import {LinearInterpolator} from './helpers/linear-interpolator'; +import {PowerCalculator} from './helpers/power-calculator'; + +export const GenericCPU = (globalConfig: ConfigParams): PluginInterface => { + const metadata = { + kind: 'execute', + }; + + /** + * Calculate CPU energy consumption based on the processor in use and measured CPU utilization values. + * Utilize power curves under the hood + */ + const execute = async (inputs: PluginParams[]) => { + const powerCurvesRootDir = globalConfig['power-curves-root-dir']; + const physicalProcessor = globalConfig['physical-processor']; + const simulatedProcessor = globalConfig['simulated-processor']; + const processorNames = shouldSimulate(simulatedProcessor) + ? [physicalProcessor, simulatedProcessor] + : [physicalProcessor]; + const powerCalculator = new PowerCalculator( + processorNames, + new CsvDirectoryReader(powerCurvesRootDir), + new LinearInterpolator() + ); + + return inputs.map((input: PluginParams) => { + const cpuUtil = input['cpu/utilization']; + const duration = input['duration']; + const power = shouldSimulate(simulatedProcessor) + ? powerCalculator.simulate( + cpuUtil, + physicalProcessor, + simulatedProcessor + ) + : powerCalculator.calculate(cpuUtil, physicalProcessor); + return { + ...input, + 'cpu/energy': calculateEnergy(power, duration), + }; + }); + }; + + const shouldSimulate = (simulatedProcessor: string) => { + return simulatedProcessor !== undefined; + }; + + const calculateEnergy = (power: number, duration: number) => { + return (power * (duration / 3600)) / 1000; + }; + + return { + metadata, + execute, + }; +}; diff --git a/src/lib/generic-cpu/models/data-table.ts b/src/lib/generic-cpu/models/data-table.ts new file mode 100644 index 0000000..c1e7c80 --- /dev/null +++ b/src/lib/generic-cpu/models/data-table.ts @@ -0,0 +1,69 @@ +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; + +export class DataTable { + private dataMap: Map = new Map(); + + constructor(data: string[][]) { + this.validateInputDataStructure(data); + this.dataMap = this.getValidatedDataMap(data); + } + + getColumnData(columnName: string): number[] { + const colData = this.dataMap.get(columnName); + if (colData === undefined) { + throw new InputValidationError( + `Column ${columnName} not found in data table` + ); + } + return colData; + } + + getSize(): number { + return this.dataMap.size; + } + + private validateInputDataStructure(data: string[][]) { + if (data === undefined || data.length === 0) { + throw new InputValidationError('input data is undefined or empty'); + } + const expectedRowLength = data[0].length; + data.forEach(row => { + if (row.length !== expectedRowLength) { + throw new InputValidationError( + 'input data misalignment: different row length' + ); + } + }); + } + + private getValidatedDataMap(data: string[][]): Map { + const dataMap: Map = new Map(); + for (let colIndex = 0; colIndex < data[0].length; colIndex++) { + const header = this.getValidatedHeader(data[0][colIndex]); + const colData: number[] = []; + for (let rowIndex = 1; rowIndex < data.length; rowIndex++) { + colData[rowIndex - 1] = this.getValidatedNumber( + data[rowIndex][colIndex] + ); + } + dataMap.set(header, colData); + } + return dataMap; + } + + private getValidatedHeader(headerString: string): string { + if (typeof headerString !== 'string' || headerString.trim() === '') { + throw new InputValidationError('Invalid header'); + } + return headerString; + } + + private getValidatedNumber(str: string): number { + const parsedValue = parseFloat(str); + if (isNaN(parsedValue)) { + throw new InputValidationError(`Failed to parse '${str}' as a number`); + } + return parsedValue; + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts index f18d984..27cf202 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -14,3 +14,4 @@ export {Sum} from './sum'; export {Coefficient} from './coefficient'; export {Divide} from './divide'; export {Regex} from './regex'; +export {GenericCPU} from './generic-cpu';