Adds required files

This commit is contained in:
Roman Galochkin 2020-02-21 15:06:19 +03:00
parent 7bc62d3934
commit 8aafb2bb6e
4 changed files with 506 additions and 0 deletions

View File

@ -0,0 +1,204 @@
const _ = require('lodash');
const { interpolate, range, min, max } = require('./functions');
class ContinuousDistribution {
/**
* @param {number[]} xs
* @param {number[]} ys
*/
constructor(xs, ys) {
if (!_.isArray(xs)) {
throw new Error('XS should be an array.');
}
if (!_.isArray(ys)) {
throw new Error('YS should be an array.');
}
if (!this.validateHasLength(xs)) {
throw new Error('You need at least one element.');
}
if (!this.validateSize(xs, ys)) {
throw new Error('Arrays of "xs" and "ys" have different sizes.');
}
const sorted = this.order(xs, ys);
this.xs = sorted.xs;
this.ys = sorted.ys;
}
/**
* Order them to make sure that xs are increasing
* @param {number[]} xs
* @param {number[]} ys
* @return {{ys: number[], xs: number[]}}
*/
order(xs, ys) {
const xsYs = xs.map((v, i) => ({ ys: ys[i], xs: v }));
const sorted = xsYs.sort((a, b) => {
if (a.xs > b.xs) return 1;
if (a.xs < b.xs) return -1;
return 0;
});
const XS = sorted.map(v => v.xs);
const YS = sorted.map(v => v.ys);
return { xs: XS, ys: YS };
}
/**
* @param {number[]} xs
* @param {number[]} ys
* @return {boolean}
*/
validateSize(xs, ys) {
return xs.length === ys.length;
}
/**
* @param xs
* @returns {boolean}
*/
validateHasLength(xs) {
return xs.length > 0;
}
/**
* @returns {number}
*/
minX() {
return this.xs[0]
}
/**
* @returns {number}
*/
maxX() {
return this.xs[this.xs.length - 1]
}
/**
* If xs=[1,2,3], and ys=[5,6,7],
* then findY(1) = 5, findY(3) = 7, findY(1.5) = 5.5
* @param {number} x
* @return {number}
*/
findY(x) {
let firstHigherIndex = this.xs.findIndex(X => X >= x);
if (firstHigherIndex < 0) return this.ys[this.ys.length - 1];
if (firstHigherIndex === 0) return this.ys[0];
let lowerOrEqualIndex = firstHigherIndex - 1;
if (lowerOrEqualIndex < 0) lowerOrEqualIndex = 0;
let needsInterpolation = this.xs[lowerOrEqualIndex] !== x;
if (needsInterpolation) {
return interpolate(
this.xs[lowerOrEqualIndex],
this.xs[firstHigherIndex],
this.ys[lowerOrEqualIndex],
this.ys[firstHigherIndex],
x
);
} else {
return this.ys[lowerOrEqualIndex];
}
}
/**
* If xs=[1,2,3], and ys=[5,6,7],
* then findX(5) = 1, findX(7) = 3, findY(5.5) = 1.5
* This should do the same thing as `findY`, but for Y.
* @param {number} y
* @return {number}
*/
findX(y) {
let firstHigherIndex = this.ys.findIndex(Y => Y >= y);
if (firstHigherIndex < 0) return this.xs[this.xs.length - 1];
if (firstHigherIndex === 0) return this.xs[0];
let lowerOrEqualIndex = firstHigherIndex - 1;
if (lowerOrEqualIndex < 0) lowerOrEqualIndex = 0;
let needsInterpolation = this.ys[lowerOrEqualIndex] !== y;
if (needsInterpolation) {
return interpolate(
this.ys[lowerOrEqualIndex],
this.ys[firstHigherIndex],
this.xs[lowerOrEqualIndex],
this.xs[firstHigherIndex],
y
);
} else {
return this.xs[lowerOrEqualIndex];
}
}
/**
* @param {number[]} xs
* @return {ContinuousDistribution}
*/
convertWithAlternativeXs(xs) {
const ys = xs.map(x => this.findY(x));
return new ContinuousDistribution(xs, ys);
}
/**
* @param {number} newLength
* @return {ContinuousDistribution}
*/
convertToNewLength(newLength) {
const _range = range(min(this.xs), max(this.xs), newLength);
return this.convertWithAlternativeXs(_range);
}
/**
* @return {number}
*/
sampleSingle() {
const y = Math.random();
return this.findX(y);
}
/**
* Poduce n samples, using ``sampleSingle`` for each.
* @param size
* @return {number[]}
*/
sample(size) {
return Array.from(Array(size), () => this.sampleSingle());
}
/**
* Finds the integral. Takes the average Y value between points,
* treating them like a triangle.
* @return {number[]}
*/
integral(params = { filterOutNaNs: false }) {
let integral = 0;
if (!params.filterOutNaNs && _.includes(this.ys, NaN)) {
return NaN;
} else if (_.includes(this.ys, Infinity) && _.includes(this.ys, -Infinity)) {
return NaN;
} else if (_.includes(this.ys, Infinity)) {
return Infinity;
} else if (_.includes(this.ys, -Infinity)) {
return -Infinity;
}
for (let i = 1; i < this.ys.length; i++) {
let thisY = this.ys[i];
let lastY = this.ys[i - 1];
let thisX = this.xs[i];
let lastX = this.xs[i - 1];
if (
_.isFinite(thisY) && _.isFinite(lastY) &&
_.isFinite(thisX) && _.isFinite(lastX)
) {
let sectionInterval = ((thisY + lastY) / 2) * (thisX - lastX);
integral = integral + sectionInterval;
}
}
return integral;
}
}
module.exports = {
ContinuousDistribution,
};

View File

@ -0,0 +1,111 @@
const { ContinuousDistribution } = require('./continuousDistribution');
const { up, down } = require('./functions');
describe('ContinuousDistribution Class', () => {
it('constructor()', () => {
const xs = up(1, 9);
const ys = up(1, 8);
expect(() => {
new ContinuousDistribution(xs, ys);
}).toThrow(/^Arrays of "xs" and "ys" have different sizes.$/);
});
it('order()', () => {
const xs = down(9, 1);
const ys = down(9, 1);
const cdf = new ContinuousDistribution(xs, ys);
expect(cdf.xs).toEqual(up(1, 9));
expect(cdf.ys).toEqual(up(1, 9));
});
it('findY()', () => {
const xs = [1, 2, 3];
const ys = [5, 6, 7];
const cdf = new ContinuousDistribution(xs, ys);
expect(cdf.findY(1)).toEqual(5);
expect(cdf.findY(1.5)).toEqual(5.5);
expect(cdf.findY(3)).toEqual(7);
expect(cdf.findY(4)).toEqual(7);
expect(cdf.findY(15)).toEqual(7);
expect(cdf.findY(-1)).toEqual(5);
});
it('findX()', () => {
const xs = [1, 2, 3];
const ys = [5, 6, 7];
const cdf = new ContinuousDistribution(xs, ys);
expect(cdf.findX(5)).toEqual(1);
expect(cdf.findX(7)).toEqual(3);
expect(cdf.findX(5.5)).toEqual(1.5);
expect(cdf.findX(8)).toEqual(3);
expect(cdf.findX(4)).toEqual(1);
});
it('convertWithAlternativeXs() when "XS" within "xs"', () => {
const xs = up(1, 9);
const ys = up(20, 28);
const cdf = new ContinuousDistribution(xs, ys);
const XS = up(3, 7);
const CDF = cdf.convertWithAlternativeXs(XS);
expect(CDF.xs).toEqual([3, 4, 5, 6, 7]);
expect(CDF.ys).toEqual([22, 23, 24, 25, 26]);
});
it('convertToNewLength()', () => {
const xs = up(1, 9);
const ys = up(50, 58);
const cdf = new ContinuousDistribution(xs, ys);
const CDF = cdf.convertToNewLength(3);
expect(CDF.xs).toEqual([1, 5, 9]);
expect(CDF.ys).toEqual([50, 54, 58]);
});
it('sample()', () => {
const xs = up(1, 9);
const ys = up(70, 78);
const cdf = new ContinuousDistribution(xs, ys);
const XS = cdf.sample(3);
expect(Number.isInteger(XS[0])).toBe(true);
expect(Number.isInteger(XS[1])).toBe(true);
expect(Number.isInteger(XS[2])).toBe(true);
});
describe('integral()', () => {
it('with regular inputs', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, 2.0, 2.0];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral();
expect(integral).toEqual(6);
});
it('with an infinity', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, Infinity, 2.0];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral();
expect(integral).toEqual(Infinity);
});
it('with negative infinity', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, -Infinity, 2.0];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral();
expect(integral).toEqual(-Infinity);
});
it('with both positive and negative infinities', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, -Infinity, Infinity];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral();
expect(integral).toEqual(NaN);
});
it('with a NaN and filterOutNaNs set to false', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, 2.0, NaN];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral({filterOutNaNs: false});
expect(integral).toEqual(NaN);
});
it('with a NaN and filterOutNaNs set to true', () => {
const xs = [0,1,2,4];
const ys = [0.0, 1.0, 2.0, NaN];
const cdf = new ContinuousDistribution(xs, ys);
const integral = cdf.integral({filterOutNaNs: true});
expect(integral).toEqual(2);
});
})
});

View File

@ -0,0 +1,136 @@
/**
* @param {number} xMin
* @param {number} xMax
* @param {number} yMin
* @param {number} yMax
* @param {number} xIntended
* @return {number}
*/
function interpolate(xMin, xMax, yMin, yMax, xIntended) {
const minProportion = (xMax - xIntended) / (xMax - xMin);
const maxProportion = (xIntended - xMin) / (xMax - xMin);
return (yMin * minProportion) + (yMax * maxProportion);
}
/**
* This should return an array of n evenly-spaced items
* between min and max, including min and max.
* range(1,5,3) = [1, 3, 5];
* range(1,5,5) = [1, 2, 3, 4, 5];
* @param {number} min
* @param {number} max
* @param {number} n
* @return {number[]}
*/
function range(min, max, n) {
if (n <= 0) throw new RangeError('n is less then zero');
if (n === Infinity) throw new RangeError('n is Infinity');
if (n === 0) return [];
if (n === 1) return [min];
if (n === 2) return [min, max];
if (min === max) return Array(n).fill(min);
n -= 1;
const diff = min - max;
const interval = Math.abs(diff / n);
const result = [];
let item = min;
do {
result.push(item);
item += interval;
} while (item <= max);
// corrects results because of math errors
if ((n + 1) - result.length === 1) {
result.push(max);
}
return result;
}
/**
* @param {number[]} arr
* @return {number}
*/
function sum(arr) {
return arr.reduce((acc, val) => acc + val, 0);
}
/**
* @param {number[]} arr
* @return {number}
*/
function mean(arr) {
return sum(arr) / arr.length;
}
/**
* @param {number[]} arr
* @return {number}
*/
function min(arr) {
let val = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < val) {
val = arr[i];
}
}
return val;
}
/**
* @param {number[]} arr
* @return {number}
*/
function max(arr) {
let val = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > val) {
val = arr[i];
}
}
return val;
}
/**
* @param {number} min
* @param {number} max
* @return {number}
*/
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* @param {number} from
* @param {number} to
* @return {number[]}
*/
function up(from, to) {
const arr = [];
for (let i = from; i <= to; i++) arr.push(i);
return arr;
}
/**
* @param {number} from
* @param {number} to
* @return {number[]}
*/
function down(from, to) {
const arr = [];
for (let i = from; i >= to; i--) arr.push(i);
return arr;
}
module.exports = {
interpolate,
min,
max,
range,
mean,
random,
up,
down,
};

View File

@ -0,0 +1,55 @@
const { interpolate } = require('./functions');
const { range } = require('./functions');
const { mean } = require('./functions');
const { min } = require('./functions');
const { max } = require('./functions');
const { random } = require('./functions');
const { up, down } = require('./functions');
describe('Functions', () => {
it('interpolate()', () => {
expect(interpolate(
10, 20,
1, 2,
15
)).toBe(1.5);
});
it('range()', () => {
expect(range(1, 5, 3)).toEqual([1, 3, 5]);
expect(range(1, 5, 5)).toEqual([1, 2, 3, 4, 5]);
expect(range(-10, 15, 2)).toEqual([-10, 15]);
expect(range(-10, 15, 3)).toEqual([-10, 2.5, 15]);
expect(range(-10.3, 17, 3)).toEqual([-10.3, 3.3499999999999996, 17]);
expect(range(-10.3, 17, 5)).toEqual([-10.3, -3.4750000000000005, 3.3499999999999996, 10.175, 17]);
expect(range(-10.3, 17.31, 3)).toEqual([-10.3, 3.504999999999999, 17.31]);
expect(range(1, 1, 3)).toEqual([1, 1, 1]);
});
it('mean()', () => {
expect(mean([1, 2, 3])).toBe(2);
expect(mean([1, 2, 3, -2])).toBe(1);
expect(mean([1, 2, 3, -2, -10])).toBe(-1.2);
});
it('min()', () => {
expect(min([1, 2, 3])).toBe(1);
expect(min([-1, -2, 0, 20])).toBe(-2);
expect(min([-1, -2, 0, 20, -2.2])).toBe(-2.2);
});
it('max()', () => {
expect(max([1, 2, 3])).toBe(3);
expect(max([-1, -2, 0, 20])).toBe(20);
expect(max([-1, -2, 0, -2.2])).toBe(0);
});
it('random()', () => {
const num = random(1, 5);
expect(num).toBeLessThanOrEqual(5);
expect(num).toBeGreaterThanOrEqual(1);
});
it('up()', () => {
expect(up(1, 5)).toEqual([1, 2, 3, 4, 5]);
expect(up(-1, 5)).toEqual([-1, 0, 1, 2, 3, 4, 5]);
});
it('down()', () => {
expect(down(5, 1)).toEqual([5, 4, 3, 2, 1]);
expect(down(5, -1)).toEqual([5, 4, 3, 2, 1, 0, -1]);
});
});