index.js (9963B)
1 import { create, all } from "mathjs"; 2 const math = create(all); 3 4 // Helper functions 5 let VERBOSE = true; 6 let print = (x) => { 7 if (VERBOSE) { 8 console.log(x); 9 } 10 }; 11 let printNode = (x) => print(JSON.stringify(x, null, 4)); 12 13 let isNumber = (x) => typeof x === "number" && isFinite(x); 14 15 let isConstantNode = (arg) => { 16 return isNumber(arg.value); 17 }; 18 19 let isNegativeNumberNode = (arg) => { 20 return ( 21 arg.op == "-" && arg.fn == "unaryMinus" && arg.args && arg.args.length == 1 22 ); 23 }; 24 let isArgLognormal = (arg) => { 25 let isFn = typeof arg.fn != "undefined"; 26 let andNameIsLognormal = isFn && arg.fn.name == "lognormal"; 27 let andHasArgs = andNameIsLognormal && !!arg.args; 28 let andHasTwoArgs = andHasArgs && arg.args.length == 2; 29 let andTwoArgsAreCorrectType = 30 andHasTwoArgs && 31 arg.args 32 .map( 33 (innerArg) => { 34 let isConstant = isConstantNode(innerArg); 35 let isNegative = isNegativeNumberNode(innerArg); 36 return isConstant || isNegative; 37 } 38 // innerArg 39 ) 40 .reduce((a, b) => a && b, true); 41 return andTwoArgsAreCorrectType; 42 }; 43 44 let getFactors = (node) => { 45 return node.args.map((arg) => { 46 if (isConstantNode(arg)) { 47 return arg.value; 48 } else if (isNegativeNumberNode(arg)) { 49 return -arg.args[0].value; 50 } 51 }); 52 }; 53 54 let createLogarithmNode = (mu, sigma) => { 55 let node1 = new math.ConstantNode(mu); 56 let node2 = new math.ConstantNode(sigma); 57 let node3 = new math.FunctionNode("lognormal", [node1, node2]); 58 return node3; 59 }; 60 61 // Main function 62 let transformerInner = (string) => { 63 let nodes = math.parse(string); 64 let transformed = nodes.transform(function (node, path, parent) { 65 // Multiplication 66 if (node.type == "OperatorNode" && node.op == "*") { 67 let hasTwoArgs = node.args && node.args.length == 2; 68 if (hasTwoArgs) { 69 // Multiplication of two lognormals 70 let areArgsLognormal = node.args 71 .map((arg) => isArgLognormal(arg)) 72 .reduce((a, b) => a && b, true); 73 74 let isFirstArgLognormal = isArgLognormal(node.args[0]); 75 let isSecondArgNumber = isConstantNode(node.args[1]); 76 let isLognormalTimesNumber = isFirstArgLognormal * isSecondArgNumber; 77 78 let isFirstArgNumber = isConstantNode(node.args[0]); 79 let isSecondArgLognormal = isArgLognormal(node.args[1]); 80 let isNumberTimesLognormal = isFirstArgNumber * isSecondArgLognormal; 81 82 if (areArgsLognormal) { 83 // lognormal times lognormal 84 let factors = node.args.map((arg) => getFactors(arg)); 85 let mean1 = factors[0][0]; 86 let std1 = factors[0][1]; 87 let mean2 = factors[1][0]; 88 let std2 = factors[1][1]; 89 90 let newMean = mean1 + mean2; 91 let newStd = Math.sqrt(std1 ** 2 + std2 ** 2); 92 return createLogarithmNode(newMean, newStd); 93 } else if (isLognormalTimesNumber) { 94 // lognormal times number 95 let lognormalFactors = getFactors(node.args[0]); 96 let mean = lognormalFactors[0]; 97 let std = lognormalFactors[1]; 98 let multiplier = node.args[1].value; 99 let logMultiplier = Math.log(multiplier); 100 let newMean = mean + logMultiplier; 101 return createLogarithmNode(newMean, std); 102 } else if (isNumberTimesLognormal) { 103 // number times lognormal 104 let lognormalFactors = getFactors(node.args[1]); 105 let mean = lognormalFactors[0]; 106 let std = lognormalFactors[1]; 107 let multiplier = node.args[0].value; 108 let logMultiplier = Math.log(multiplier); 109 let newMean = mean + logMultiplier; 110 return createLogarithmNode(newMean, std); 111 } 112 } 113 } else if (node.type == "OperatorNode" && node.op == "/") { 114 let hasTwoArgs = node.args && node.args.length == 2; 115 if (hasTwoArgs) { 116 let areArgsLognormal = node.args 117 .map((arg) => isArgLognormal(arg)) 118 .reduce((a, b) => a && b, true); 119 120 let isFirstArgLognormal = isArgLognormal(node.args[0]); 121 let isSecondArgNumber = isConstantNode(node.args[1]); 122 let isLognormalDividedByNumber = 123 isFirstArgLognormal * isSecondArgNumber; 124 125 let isFirstArgNumber = isConstantNode(node.args[0]); 126 let isSecondArgLognormal = isArgLognormal(node.args[1]); 127 let isNumberDividedByLognormal = 128 isFirstArgNumber * isSecondArgLognormal; 129 130 if (areArgsLognormal) { 131 let factors = node.args.map((arg) => getFactors(arg)); 132 let mean1 = factors[0][0]; 133 let std1 = factors[0][1]; 134 let mean2 = factors[1][0]; 135 let std2 = factors[1][1]; 136 137 let newMean = mean1 - mean2; 138 let newStd = Math.sqrt(std1 ** 2 + std2 ** 2); 139 return createLogarithmNode(newMean, newStd); 140 } else if (isLognormalDividedByNumber) { 141 let lognormalFactors = getFactors(node.args[0]); 142 let mean = lognormalFactors[0]; 143 let std = lognormalFactors[1]; 144 let multiplier = node.args[1].value; 145 let logMultiplier = Math.log(multiplier); 146 let newMean = mean - logMultiplier; 147 return createLogarithmNode(newMean, std); 148 } else if (isNumberDividedByLognormal) { 149 let lognormalFactors = getFactors(node.args[1]); 150 let mean = lognormalFactors[0]; 151 let std = lognormalFactors[1]; 152 let multiplier = node.args[0].value; 153 let logMultiplier = Math.log(multiplier); 154 let newMean = -mean + logMultiplier; 155 return createLogarithmNode(newMean, std); 156 } 157 } 158 } 159 if (node.type == "ParenthesisNode") { 160 if ( 161 !!node.content && 162 !!node.content.fn && 163 node.content.fn.name == "lognormal" 164 ) { 165 return node.content; 166 } 167 } 168 return node; 169 }); 170 171 return transformed; 172 }; 173 174 const normal95confidencePoint = 1.6448536269514722; 175 176 let from90PercentCI = (low, high) => { 177 let logLow = Math.log(low); 178 let logHigh = Math.log(high); 179 let mu = (logLow + logHigh) / 2; 180 let sigma = (logHigh - logLow) / (2.0 * normal95confidencePoint); 181 return [mu, sigma]; 182 }; 183 184 let to90PercentCI = (mu, sigma) => { 185 let logHigh = mu + normal95confidencePoint * sigma; 186 let logLow = mu - normal95confidencePoint * sigma; 187 let high = Math.exp(logHigh); 188 let low = Math.exp(logLow); 189 return [low, high]; 190 }; 191 192 let simplePreprocessor = (string) => { 193 // left for documentation purposes only 194 function replacer(match, p1, p2) { 195 print(match); 196 // p1 is nondigits, p2 digits, and p3 non-alphanumericsa 197 print([p1, p2]); 198 let result = from90PercentCI(p1, p2); 199 return `lognormal(${result[0]}, ${result[1]})`; 200 } 201 let newString = string.replace(/(\d+) to (\d+)/g, replacer); 202 print(newString); 203 return newString; // abc - 12345 - #$*% 204 }; 205 206 // simplePreprocessor("1 to 10 + 1 to 20"); 207 208 let toLognormalParameters = (node) => { 209 if (isArgLognormal(node)) { 210 let factors = getFactors(node); 211 // print(node); 212 // print(factors); 213 return [factors[0], factors[1]]; 214 } else { 215 return null; 216 } 217 }; 218 219 let customToStringHandlerTwoDecimals = (node, options) => { 220 if (node.type == "ConstantNode") { 221 return node.value.toFixed(2); 222 } 223 }; 224 225 let preprocessor = (string, print = console.log) => { 226 // work in progress, currently not working 227 let regex = /([\d]+\.?[\d]*|\.[\d]+) to ([\d]+\.?[\d]*|\.[\d]+)/g; 228 function replacer(match, p1, p2) { 229 let result = from90PercentCI(p1, p2); 230 return `lognormal(${result[0]}, ${result[1]})`; 231 } 232 let newString = string.replace(regex, replacer); 233 if (newString != string) 234 print( 235 `\t= ${math 236 .parse(newString) 237 .toString({ handler: customToStringHandlerTwoDecimals })}` 238 ); 239 return newString; // abc - 12345 - #$*% 240 }; 241 // preprocessor("1.2 to 10.5 * 1.1 to 20 * 1 to 2.5 * 1 to 5"); 242 243 let customToStringHandlerToGuesstimateSyntax = (node, options) => { 244 if (isArgLognormal(node)) { 245 let factors = getFactors(node); 246 // print(node); 247 // print(factors); 248 let ninetyPercentCI = to90PercentCI(factors[0], factors[1]); 249 return `~${ninetyPercentCI[0]} to ~${ninetyPercentCI[1]}`; 250 } 251 }; 252 253 let toPrecision2 = (f) => f.toPrecision(2); 254 let numToString = (x) => 255 x < 10 256 ? toPrecision2(x).toLocaleString() 257 : BigInt(Math.round(toPrecision2(x))).toString(); 258 259 let toShortGuesstimateString = (node) => { 260 if (isArgLognormal(node)) { 261 let factors = getFactors(node); 262 // print(node); 263 // print(factors); 264 let ninetyPercentCI = to90PercentCI(factors[0], factors[1]); 265 return `${numToString(ninetyPercentCI[0])} to ${numToString( 266 ninetyPercentCI[1] 267 )}`; 268 } else { 269 return null; 270 } 271 }; 272 273 let to90CIArray = (node) => { 274 if (isArgLognormal(node)) { 275 let factors = getFactors(node); 276 // print(node); 277 // print(factors); 278 let ninetyPercentCI = to90PercentCI(factors[0], factors[1]); 279 return [ninetyPercentCI[0], ninetyPercentCI[1]]; 280 } else { 281 return null; 282 } 283 }; 284 285 export function transformer(string, print = console.log) { 286 string = preprocessor(string, print); 287 let transformerOutput = transformerInner(string); 288 let stringNew = transformerOutput.toString(); 289 while (stringNew != string) { 290 print( 291 `\t-> ${transformerOutput.toString({ 292 handler: customToStringHandlerTwoDecimals, 293 })}` 294 ); 295 string = stringNew; 296 transformerOutput = transformerInner(string); 297 stringNew = transformerOutput.toString(); 298 } 299 let squiggleString = stringNew; 300 let lognormalParameters = toLognormalParameters(transformerOutput); 301 let shortGuesstimateString = toShortGuesstimateString(transformerOutput); 302 let array90CI = to90CIArray(transformerOutput); 303 // console.log(transformerOutput); 304 let result = { 305 squiggleString: squiggleString, 306 lognormalParameters: lognormalParameters, 307 shortGuesstimateString: shortGuesstimateString, 308 array90CI: array90CI, 309 }; 310 return result; 311 }