diff --git a/src/models/serie.js b/src/models/serie.js new file mode 100644 index 0000000..3b29787 --- /dev/null +++ b/src/models/serie.js @@ -0,0 +1,352 @@ +export class Serie { + /** + * A class representing a series of values + * This class is an Array with usefull extra methods and properties + */ + name:?string + type: string + content: string + containString: boolean + containBool: boolean + containNumber: boolean + data: | $ReadOnlyArray + | $ReadOnlyArray + | $ReadOnlyArray + + constructor (inputArray: Array | Array | Array , + inputName?:string = '') { + this.name = inputName + const iMax = inputArray.length + let i = 0 + // the rest of the constructor is inputArray content checking + let numberData: Array = [] + let stringData: Array = [] + let booleanData: Array = [] + for (; i < iMax; i++) { + if ((typeof inputArray[i]) === 'number' | + (inputArray[i] === undefined) | + (inputArray[i] === null)) { + numberData.push(inputArray[i]) + } + if ((typeof inputArray[i]) === 'string' | + (inputArray[i] === undefined) | + (inputArray[i] === null) | + (inputArray[i] === '')) { + stringData.push(inputArray[i]) + } + if ((typeof inputArray[i]) === 'boolean' | + (inputArray[i] === undefined) | + (inputArray[i] === null)) { + booleanData.push(inputArray[i]) + } else if ((typeof inputArray[i] === 'object')) { + throw new Error("Wrong data type for Serie's input") + } + } + let numberLength = numberData.length + let stringLength = stringData.length + let booleanLength = booleanData.length + let argMax = [numberLength, stringLength, booleanLength].map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1] + + if (argMax === 2) { + this.containBool = true + this.containString = false + this.containNumber = false + this.data = booleanData + for (let elt of stringData) { + if (typeof elt === 'string') { + throw new Error("Serie's input contains more than one type") + } + } + for (let elt of numberData) { + if (typeof elt === 'number') { + throw new Error("Serie's input contains more than one type") + } + } + } + if (argMax === 1) { + this.containBool = false + this.containString = true + this.containNumber = false + this.data = stringData + for (let elt of booleanData) { + if (typeof elt === 'boolean') { + throw new Error("Serie's input contains more than one type") + } + } + for (let elt of numberData) { + if (typeof elt === 'number') { + throw new Error("Serie's input contains more than one type") + } + } + } + if (argMax === 0) { + this.containBool = false + this.containString = false + this.containNumber = true + this.data = numberData + for (let elt of booleanData) { + if (typeof elt === 'boolean') { + throw new Error("Serie's input contains more than one type") + } + } + for (let elt of stringData) { + if (typeof elt === 'string') { + throw new Error("Serie's input contains more than one type") + } + } + } + } + // methods + getIndex (val : string | number | boolean) : Array { + /** + * Return the indexes at which val is present + */ + const indexes = [] + let i = 0 + const iMax = this.length // optimize loop + for (; i < iMax; i++) { + if (this.data[i] === val) { + indexes.push(i) + } + } + return indexes + } + getValue (indexArray : Array) : Array|Array|Array { + /** + * Return the values present at each index of indexArray + */ + + let values : Array = [] + let i = 0 + const iMax = this.length + for (; i < iMax; i++) { + for (const j of indexArray) { + if (i === j) { + values.push(this.data[i]) + } + } + } + return values + } + countValue (value : number | string | boolean) : number { + /** + * Return the number of occurence of value in array + */ + let count = 0 + const iMax = this.length // optimize loop + let i = 0 + for (; i < iMax; i++) { + if (this.data[i] === value) { + count += 1 + } + } + return count + } + freqValue (value: number | string | boolean) : number { + /** + * Return the frequency of a given value in array + */ + return (this.countValue(value) / this.length) + } + fillNull (method: string = 'maxFreq') : Serie { + /** + * Complete uncomplete values in Serie and return a completed Serie + * following the given method : 'median', 'mean', 'maxFreq' + */ + if (this.isComplete === false) { + // precomputing maxOcc + let validValue = this.validValue + let uniqValidValue = new Serie(validValue.uniqValue) + let freq = [] + for (let val of uniqValidValue.data) { + freq.push(this.freqValue(val)) + } + let freqSerie = new Serie(freq) + let maxOcc = uniqValidValue.getValue(freqSerie.argMax)[0] + + if (this.containNumber) { + const iMax = this.length + let i = 0 + let newData = [] + // precomputing mean and median + let mean = validValue.mean + let median = validValue.median + + for (; i < iMax; i++) { + if (this.data[i] === undefined | this.data[i] === null) { + if (method === 'maxFreq') { + newData.push(maxOcc) + } + if (method === 'mean') { + newData.push(mean) + } + if (method === 'median') { + newData.push(median) + } + } else { + newData.push(this.data[i]) + } + } + return new Serie(newData) + } + if (this.containString | + this.containBool) { + const iMax = this.length + let i = 0 + let newData = [] + for (; i < iMax; i++) { + if (this.data[i] === undefined | + this.data[i] === null | + this.data[i] === '') { + newData.push(maxOcc) + } else { + newData.push(this.data[i]) + } + } + return new Serie(newData) + } + } else { + return this + } + } + // properties + get length () : number { + return this.data.length + } + get type () : string { + return 'Serie' + } + get content () : string { + if (this.containBool) { + return 'boolean' + } + if (this.containNumber) { + return 'number' + } + if (this.containString) { + return 'string' + } + } + get uniqValue () : Array|Array|Array { + const uValues = new Set(this.data) + return [...uValues] + } + get max () : number { + if (this.containNumber) { + return Math.max.apply(null, this.data) + } else { + throw new Error('max works only on Serie that contains number') + } + } + get argMax () : number { + if (this.containNumber) { + return this.getIndex(this.max) + } else { + throw new Error('ArgMax works only on Serie that contains number') + } + } + get min () : number { + if (this.containNumber) { + return Math.min.apply(null, this.data) + } else { + throw new Error('min works only on Serie that contains number') + } + } + get argMin () : number { + if (this.containNumber) { + return this.getIndex(this.min) + } else { + throw new Error('argMin works only on Serie that contains number') + } + } + get mean () : number { + if (this.containNumber) { + const iMax = this.length + let sum = 0 + let numValid = 0 + let i = 0 + for (; i < iMax; i++) { + if (this.data[i]) { + numValid += 1 + sum += this.data[i] + } + } + return sum / numValid + } else { + throw new Error('mean works only on Serie that contains number') + } + } + get median () : number { + if (this.containNumber) { + let values = this.validValue.data + values.sort((a, b) => a - b) + let half = Math.floor(values.length / 2) + if (values.length % 2) { + return values[half] + } else { + return (values[half - 1] + values[half]) / 2.0 + } + } else { + throw new Error('median works only on Serie that contains number') + } + } + get isComplete () : boolean { + let completion = true + const iMax = this.length + + if (this.containBool || this.containNumber) { + let i = 0 + for (; i < iMax; i++) { + if (this.data[i] === undefined || this.data[i] === null) { + completion = false + } + } + } + if (this.containString) { + let i = 0 + for (; i < iMax; i++) { + if (this.data[i] === undefined || + this.data[i] === null || + this.data[i] === '') { + completion = false + } + } + } + return completion + } + get validValue () : Serie { + if (this.isComplete) { + return this + } else { + const iMax = this.length + let validValue = [] + if (this.containNumber) { + let i = 0 + for (; i < iMax; i++) { + if (this.data[i] || + this.data[i] === 0) { + validValue.push(this.data[i]) + } + } + } + if (this.containBool) { + let i = 0 + for (; i < iMax; i++) { + if (this.data[i] || + this.data[i] === false) { + validValue.push(this.data[i]) + } + } + } + if (this.containString) { + let i = 0 + for (; i < iMax; i++) { + if (this.data[i]) { + validValue.push(this.data[i]) + } + } + } + return new Serie(validValue) + } + } +} diff --git a/test/models/serie.test.js b/test/models/serie.test.js new file mode 100644 index 0000000..1430829 --- /dev/null +++ b/test/models/serie.test.js @@ -0,0 +1,207 @@ +import { expect } from 'chai' +import { Serie } from '../../src/models/serie' +describe('Serie', () => { + const stringArray = ['m', 'f', 'f', 'f', 'm', 'm', 'm', 'm', 'f', 'f', 'f', 'f', 'm', 'm', 'f', 'f', 'm', 'm'] + const numberArray = [0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1] + const booleanArray = [false, true, true, true, false, false, false, false, true, true, true, true, false] + const mixedTypeArray = [1, 2, 3, 4, 5, true, false, 'toto', 'hi'] + const wrongTypeArray = [[1, 2], { 'key': 'value' }, [[[1]]]] + const stringSerie = new Serie(stringArray, 'string') + const numberSerie = new Serie(numberArray, 'number') + const booleanSerie = new Serie(booleanArray, 'boolean') + const uncompleteString = new Serie(['m', 'f', '', null, undefined, 'm']) + const uncompleteNumber = new Serie([1, 1, 0, undefined, 0, 1, null]) + const uncompleteBoolean = new Serie([true, true, null, false, undefined, true]) + + it('Should create a Serie from an Array of number', () => { + expect(numberSerie.length).to.be.at.least(1) + }) + it('Should create a Serie from an Array of string', () => { + expect(stringSerie.length).to.be.at.least(1) + }) + it('Should create a Serie from an Array of boolean', () => { + expect(booleanSerie.length).to.be.at.least(1) + }) + it('Should create a Serie without name', () => { + const noName = new Serie(numberArray) + expect(noName.length).to.be.at.least(1) + expect(noName.name).to.be.equal('') + }) + it('Should reject Serie creation from input of mixed type', () => { + try { + new Serie(mixedTypeArray, 'mixed type') + } catch (error) { + expect(error.message).to.be.equal("Serie's input contains more than one type") + } + }) + it('Should reject Serie creation from input of wrong type', () => { + try { + new Serie(wrongTypeArray, 'wrong type') + } catch (error) { + expect(error.message).to.be.equal("Wrong data type for Serie's input") + } + }) + describe('getIndex', () => { + it('Should get the indexes of a number value', () => { + expect(numberSerie.getIndex(0)).to.deep.equal([0, 4, 5, 6, 7, 12, 13, 14, 16]) + }) + it('Should get the indexes of a string value', () => { + expect(stringSerie.getIndex('m')).to.deep.equal([0, 4, 5, 6, 7, 12, 13, 16, 17]) + }) + it('Should get the indexes of a boolean value', () => { + expect(booleanSerie.getIndex(true)).to.deep.equal([1, 2, 3, 8, 9, 10, 11]) + }) + }) + describe('getValue', () => { + it('Should get values from a list of indexes', () => { + expect(booleanSerie.getValue([0, 1, 2])).to.deep.equal([false, true, true]) + expect(numberSerie.getValue([0, 1, 2])).to.deep.equal([0, 1, 1]) + expect(stringSerie.getValue([0, 1, 2])).to.deep.equal(['m', 'f', 'f']) + }) + }) + describe('countValue', () => { + it('Should count values in an array of number', () => { + expect(numberSerie.countValue(0)).to.be.equal(9) + }) + it('Should count values in an array of string', () => { + expect(stringSerie.countValue('m')).to.be.equal(9) + }) + it('Should count values in an array of boolean', () => { + expect(booleanSerie.countValue(true)).to.be.equal(7) + }) + }) + describe('freqValue', () => { + it('Should give the occurency frequence of a given value in an array of number', () => { + expect(numberSerie.freqValue(0)).to.be.equal(0.5) + expect(numberSerie.freqValue(1)).to.be.equal(0.5) + }) + it('Should give the occurency frequence of a given value in an array of string', () => { + expect(stringSerie.freqValue('m')).to.be.equal(0.5) + expect(stringSerie.freqValue('f')).to.be.equal(0.5) + }) + it('Should give the occurency frequence of a given value in an array of boolean', () => { + expect(booleanSerie.freqValue(true)).to.be.equal(0.5384615384615384) + expect(booleanSerie.freqValue(false)).to.be.equal(0.46153846153846156) + }) + }) + describe('fillNull', () => { + it('Should complete null values in a Serie of number', () => { + expect(uncompleteNumber.fillNull().data).to.deep.equal([1, 1, 0, 1, 0, 1, 1]) + }) + it('Should complete null values in a Serie of string', () => { + expect(uncompleteString.fillNull().data).to.deep.equal(['m', 'f', 'm', 'm', 'm', 'm']) + }) + it('Should complete null values in a Serie of boolean', () => { + expect(uncompleteBoolean.fillNull().data).to.deep.equal([true, true, true, false, true, true]) + }) + }) + describe('length', () => { + it('Should return the length of a Serie of number', () => { + expect(numberSerie.length).to.be.equal(18) + }) + it('Should return the length of a Serie of string', () => { + expect(stringSerie.length).to.be.equal(18) + }) + it('Should return the length of a Serie of boolean', () => { + expect(booleanSerie.length).to.be.equal(13) + }) + }) + describe('type', () => { + it('Should return the type of a Serie, whatever it contains', () => { + expect(numberSerie.type).to.be.equal('Serie') + expect(stringSerie.type).to.be.equal('Serie') + expect(booleanSerie.type).to.be.equal('Serie') + }) + }) + describe('content', () => { + it("Should return the content's type of a Serie, whatever it contains", () => { + expect(numberSerie.content).to.be.equal('number') + expect(stringSerie.content).to.be.equal('string') + expect(booleanSerie.content).to.be.equal('boolean') + }) + }) + describe('containBool', () => { + it('Should return true for a Serie that contains boolean, false otherwise', () => { + expect(booleanSerie.containBool).to.be.true + expect(numberSerie.containBool).to.be.false + expect(stringSerie.containBool).to.be.false + }) + }) + describe('containString', () => { + it('Should return true for a Serie that contains string, false otherwise', () => { + expect(booleanSerie.containString).to.be.false + expect(numberSerie.containString).to.be.false + expect(stringSerie.containString).to.be.true + }) + }) + describe('containNumber', () => { + it('Should return true for a Serie that contains number, false otherwise', () => { + expect(booleanSerie.containNumber).to.be.false + expect(numberSerie.containNumber).to.be.true + expect(stringSerie.containNumber).to.be.false + }) + }) + describe('uniqValue', () => { + it('Should return unique values in the Serie', () => { + expect(booleanSerie.uniqValue).to.contain(true, false) + expect(numberSerie.uniqValue).to.contain(0, 1) + expect(stringSerie.uniqValue).to.contain('m', 'f') + }) + }) + describe('max', () => { + it('Should return the Max of an array of number', () => { + expect(numberSerie.max).to.be.equal(1) + }) + }) + describe('min', () => { + it('Should return the Min of an array of number', () => { + expect(numberSerie.min).to.be.equal(0) + }) + }) + describe('argMax', () => { + it('Should return the argument of the Max of an array of number', () => { + expect(numberSerie.argMax).to.deep.equal([1, 2, 3, 8, 9, 10, 11, 15, 17]) + }) + }) + describe('argMin', () => { + it('Should return the Min of an array of number', () => { + expect(numberSerie.argMin).to.deep.equal([0, 4, 5, 6, 7, 12, 13, 14, 16]) + }) + }) + describe('mean', () => { + it('Should return the Mean of an array of number', () => { + let numberArr = [1, 4, 6, 9, 12, 15] + let serie = new Serie(numberArr) + expect(serie.mean).to.be.equal(7.833333333333333) + expect(numberSerie.mean).to.be.equal(1) + }) + }) + describe('median', () => { + it('Should return the Median of an array of number', () => { + expect(numberSerie.median).to.be.equal(0.5) + }) + }) + describe('isComplete', () => { + it('Should return false for uncomplete array', () => { + expect(uncompleteBoolean.isComplete).to.be.false + expect(uncompleteNumber.isComplete).to.be.false + expect(uncompleteBoolean.isComplete).to.be.false + }) + it('Should return true for complete array', () => { + expect(booleanSerie.isComplete).to.be.true + expect(numberSerie.isComplete).to.be.true + expect(stringSerie.isComplete).to.be.true + }) + }) + describe('validValue', () => { + it('Should find the valid values (non Null) in a number Serie', () => { + expect(uncompleteNumber.validValue.data).to.deep.equal([1, 1, 0, 0, 1]) + }) + it('Should find the valid values (non Null) in a string Serie', () => { + expect(uncompleteString.validValue.data).to.deep.equal(['m', 'f', 'm']) + }) + it('Should find the valid values (non Null) in a boolean Serie', () => { + expect(uncompleteBoolean.validValue.data).to.deep.equal([true, true, false, true]) + }) + }) +})