all files / three-point/ source.js

95.65% Statements 44/46
100% Branches 7/7
85.71% Functions 12/14
94.44% Lines 34/36
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66                                                                
import gaussian from 'gaussian';
 
export default class ThreePoint {
  constructor(items, confidenceLevel) {
    this.items = items;
    this.confidenceLevel = confidenceLevel || 0.9;
    this.estimate();
  }
 
  set items(value) {
    value.forEach(({ optimistic, pessimistic, likely }) => {
      if (isNaN(optimistic) || isNaN(pessimistic) || isNaN(likely)) {
        throw new Error('Encountered a non-number in estimate items');
      }
    });
 
    this._items = value;
    this.estimate();
  }
 
  set confidenceLevel(value) {
    this._criticalValue = this.findCriticalValue(value);
    this.estimate();
  }
 
  get expected() {
    return Math.round(this._expected * 10) / 10;
  }
 
  get pessimistic() {
    return Math.round(this._pessimistic * 10) / 10;
  }
 
  get optimistic() {
    return Math.round(this._optimistic * 10) / 10;
  }
 
  findCriticalValue(value) {
    const distribution = gaussian(0, 1);
    return distribution.ppf((1 + value) / 2);
  }
 
  estimate() {
    const estimates = this._items.map(this.estimateItem);
 
    const standardDeviation = Math.sqrt(estimates
      .map(x => Math.pow(x.standardDeviation, 2))
      .reduce((a, b) => a + b));
 
    const expected = estimates
      .map(x => x.expected)
      .reduce((a, b) => a + b);
 
    this._expected = expected;
    this._pessimistic = expected + (standardDeviation * this._criticalValue);
    this._optimistic = expected - (standardDeviation * this._criticalValue);
  }
 
  estimateItem({ optimistic, pessimistic, likely }) {
    return {
      standardDeviation: (pessimistic - optimistic) / 6,
      expected: (optimistic + 4 * likely + pessimistic) / 6,
    };
  }
}