GoalSeek.js 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. const IsNanError = TypeError('resulted in NaN');
  2. const FailedToConvergeError = Error('failed to converge');
  3. const InvalidInputsError = Error('invalid inputs');
  4. export default async function GoalSeek({
  5. fn,
  6. fnParams,
  7. percentTolerance,
  8. customToleranceFn,
  9. maxIterations,
  10. maxStep,
  11. goal,
  12. independentVariableIdx,
  13. }) {
  14. if (typeof customToleranceFn !== 'function') {
  15. if (!percentTolerance) {
  16. throw InvalidInputsError;
  17. }
  18. }
  19. let g;
  20. let y;
  21. let y1;
  22. let oldGuess;
  23. let newGuess;
  24. let res;
  25. const absoluteTolerance = ((percentTolerance || 0) / 100) * goal;
  26. // iterate through the guesses
  27. for (let i = 0; i < maxIterations; i++) {
  28. // define the root of the function as the error
  29. res = await fn(...fnParams);
  30. y = res - goal;
  31. if (isNaN(y)) throw IsNanError;
  32. // was our initial guess a good one?
  33. if (typeof customToleranceFn !== 'function') {
  34. if (Math.abs(y) <= Math.abs(absoluteTolerance)) return fnParams[independentVariableIdx];
  35. } else {
  36. if (customToleranceFn(res)) return fnParams[independentVariableIdx];
  37. }
  38. // set the new guess, correcting for maxStep
  39. oldGuess = fnParams[independentVariableIdx];
  40. newGuess = oldGuess + y;
  41. if (Math.abs(newGuess - oldGuess) > maxStep) {
  42. if (newGuess > oldGuess) {
  43. newGuess = oldGuess + maxStep;
  44. } else {
  45. newGuess = oldGuess - maxStep;
  46. }
  47. }
  48. fnParams[independentVariableIdx] = newGuess;
  49. // re-run the fn with the new guess
  50. y1 = (await fn(...fnParams)) - goal;
  51. if (isNaN(y1)) throw IsNanError;
  52. // calculate the error
  53. g = (y1 - y) / y;
  54. if (g === 0) g = 0.0001;
  55. // set the new guess based on the error, correcting for maxStep
  56. newGuess = oldGuess - y / g;
  57. if (maxStep && Math.abs(newGuess - oldGuess) > maxStep) {
  58. if (newGuess > oldGuess) {
  59. newGuess = oldGuess + maxStep;
  60. } else {
  61. newGuess = oldGuess - maxStep;
  62. }
  63. }
  64. fnParams[independentVariableIdx] = newGuess;
  65. }
  66. // done with iterations, and we failed to converge
  67. throw FailedToConvergeError;
  68. }
  69. // const fn = (x, y) => x / y;
  70. // const fnParams = [2037375, 15897178];
  71. // const customToleranceFn = (x) => {
  72. // return x < 1;
  73. // };
  74. // try {
  75. // const result = goalSeek({
  76. // fn,
  77. // fnParams,
  78. // customToleranceFn,
  79. // maxIterations: 1000,
  80. // maxStep: 0.01,
  81. // goal: 0.15,
  82. // independentVariableIdx: 0,
  83. // });
  84. // console.log(`result: ${result}`);
  85. // } catch (e) {
  86. // console.log("error", e);
  87. // }