Created
May 7, 2020 07:38
-
-
Save Validark/f677cfa22cf2d67d3ff902d42ff9bc14 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| interface BinaryOperation { | |
| symbols: ReadonlyArray<string>; | |
| call: (a: number, b: number) => number; | |
| precedence: number; | |
| leftAssociative: boolean; | |
| } | |
| // function* parseStr(str: string) { | |
| // let lastIndex = 0; | |
| // const reg = /(?<whitespace>\s+)|(?<hex>0x[0-9A-Fa-f]+\.?[0-9A-Fa-f]*)|(?<octal>0o[0-7]+\.?[0-7]*)|(?<binary>0b[01]+\.?[01]*)|(?<decimal>\d+\.?\d*)|(?<binaryOperator>[-\+\*/])/gu; | |
| // let match = reg.exec(str); | |
| // while (match && match.index === lastIndex) { | |
| // ({ lastIndex } = reg); | |
| // const [type, symbol] = Object.entries(match.groups!).find(([, a]) => a)!; | |
| // if (type !== "whitespace") yield { type, symbol }; | |
| // match = reg.exec(str); | |
| // } | |
| // if (lastIndex !== str.length) throw `Invalid token at: ${str.slice(lastIndex)}`; | |
| // } | |
| // for (const token of parseStr("5 -3")) { | |
| // console.log(token); | |
| // } | |
| const NaN = 0 / 0; | |
| namespace Operators { | |
| let precedence = 0; | |
| export const ADD: BinaryOperation = { | |
| symbols: ["+"], | |
| call: (a, b) => a + b, | |
| precedence, | |
| leftAssociative: true, | |
| }; | |
| // We don't have UNM, we just implicitly do `SUB(0, x)` instead | |
| export const SUB: BinaryOperation = { | |
| symbols: ["-"], | |
| call: (a, b) => a - b, | |
| precedence, | |
| leftAssociative: true, | |
| }; | |
| ++precedence; | |
| export const MUL: BinaryOperation = { | |
| symbols: ["*"], | |
| call: (a, b) => a * b, | |
| precedence, | |
| leftAssociative: true, | |
| }; | |
| export const DIV: BinaryOperation = { | |
| symbols: ["/"], | |
| call: (a, b) => a / b, | |
| precedence, | |
| leftAssociative: true, | |
| }; | |
| export const MOD: BinaryOperation = { | |
| symbols: ["%", "mod"], | |
| call: (a, b) => a % b, | |
| precedence, | |
| leftAssociative: true, | |
| }; | |
| ++precedence; | |
| export const POW: BinaryOperation = { | |
| symbols: ["^", "**"], | |
| call: (a, b) => a ** b, | |
| precedence, | |
| leftAssociative: false, | |
| }; | |
| } | |
| const Constants = new Map<string, number>([ | |
| ["Pi", math.pi], | |
| ["E", math.exp(1)], | |
| ["Phi", (1 + math.sqrt(5)) / 2], | |
| ["Tau", 2 * math.pi], | |
| ["Rho", 0.5 + math.sqrt(69) / 18], | |
| ["Huge", math.huge], | |
| ["Infinity", math.huge], | |
| ["-Infinity", -math.huge], | |
| ["NaN", NaN], | |
| ]); | |
| function round(num: number, place = 1, threshold = 0.5) { | |
| // Rounds number to the nearest Place with a given Threshold towards ceiling | |
| // @param number Number the value to round | |
| // @param number Place Number must be rounded to a multiple of Place | |
| // @default = 1 | |
| // @param Threshold How biased it should be towards rounding up | |
| // @default = 0.5 | |
| // When Threshold == 0, it Floors | |
| // When Threshold == 1, it Ceils | |
| // When Threshold == 0.5 (default), it Rounds to the nearest | |
| num = num + place * threshold; | |
| return num - (num % place); | |
| } | |
| function floor(num: number, place?: number, precisionLeeway = 0) { | |
| return round(num, place, precisionLeeway); | |
| } | |
| function ceil(num: number, place?: number, precisionLeeway = 0) { | |
| return round(num, place, 1 - precisionLeeway); | |
| } | |
| const Functions = new Map<string, (...args: Array<number>) => number>([ | |
| ["Cbrt", n => n ** (1 / 3)], | |
| ["Log_e", n => math.log(n)], | |
| ["Ln", n => math.log(n)], | |
| ["Cot", n => 1 / math.tan(n)], | |
| ["Acot", a => math.atan(1 / a)], | |
| ["Csc", a => 1 / math.sin(a)], | |
| ["Acsc", a => math.asin(1 / a)], | |
| ["Sec", a => 1 / math.cos(a)], | |
| ["Asec", a => math.acos(1 / a)], | |
| ["Round", round], | |
| ["Floor", floor], | |
| ["Ceil", ceil], | |
| ]); | |
| for (const [k, v] of Object.entries(math)) | |
| if (k !== "randomseed" && k !== "floor" && k !== "ceil" && typeIs(v, "function")) | |
| Functions.set(k.gsub("^%l", (s: string) => s.upper(), 1)[0], v); | |
| for (const k of ["Sin", "Cos", "Tan", "Tan2", "Cot", "Csc", "Sec"]) Functions.set(`Arc${k}`, Functions.get(`A${k}`)!); | |
| /** Duplicates keys in a Map of the form (Case => any) to also allow (case => any) and (CASE => any) */ | |
| function DecasifyMap<T extends Map<string, unknown>>(map: T) { | |
| for (const key of map.keys()) { | |
| const value = map.get(key); | |
| map.set(key.lower(), value); | |
| map.set(key.upper(), value); | |
| } | |
| } | |
| DecasifyMap(Constants); | |
| DecasifyMap(Functions); | |
| Constants.set(string.char(227), Constants.get("pi")!); | |
| Constants.set(string.char(231), Constants.get("tau")!); | |
| Constants.set(string.char(236), Constants.get("huge")!); | |
| Constants.set(string.char(237), Constants.get("phi")!); | |
| namespace Tokenizer { | |
| export type Token = | |
| | { readonly kind: "Operator"; readonly value: BinaryOperation } | |
| | { readonly kind: "Number"; readonly value: string } | |
| | { readonly kind: "GroupingOpen" | "GroupingClose" | "Erroneous"; readonly value: string } | |
| | { readonly kind: "Function"; readonly value: (...args: any) => void } | |
| | { readonly kind: "LogFunction" | "Comma" } | |
| | { readonly kind: "Constant"; readonly value: number }; | |
| const tokenMatchers: Array<{ | |
| readonly kind: Token["kind"] | "Whitespace"; | |
| readonly matcher: string; | |
| }> = [ | |
| { kind: "Whitespace", matcher: "%s+" }, | |
| { kind: "Comma", matcher: "," }, | |
| { kind: "Number", matcher: "%-?%d+%.?%d*[Ee_]?" }, // 111.11111 | |
| { kind: "Number", matcher: "%-?0[Xx]%x+%.%x*[Ee_]?" }, // 0x12.d03 | |
| { kind: "Number", matcher: "%-?0[Oo][0-7]+%.?[0-7]*[Ee_]?" }, // 0o12.603 | |
| { kind: "Number", matcher: "%-?0[Ob][0-1]+%.?[0-1]*[Ee_]?" }, // 0b01.02 | |
| { kind: "GroupingOpen", matcher: "[%(%[{]" }, | |
| { kind: "GroupingClose", matcher: "[%)%]}]" }, | |
| { kind: "LogFunction", matcher: "[Ll]og_" }, | |
| { kind: "LogFunction", matcher: "LOG_" }, | |
| ]; | |
| for (const operator of [Operators.ADD, Operators.SUB, Operators.MUL, Operators.DIV, Operators.MOD, Operators.POW]) { | |
| for (const symbol of operator.symbols) { | |
| tokenMatchers.push({ kind: "Operator", matcher: symbol.gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")[0] }); | |
| } | |
| } | |
| for (const [kind, map] of [ | |
| ["Function", Functions], | |
| ["Constant", Constants], | |
| ] as const) { | |
| for (const [key] of map) | |
| tokenMatchers.push({ kind, matcher: key.gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")[0] }); | |
| } | |
| // Lock all matcher strings to the beginning of the current spot, and wrap in parens to capture the group | |
| for (const tokenString of tokenMatchers) (tokenString as { matcher: string }).matcher = `^(${tokenString.matcher})`; | |
| export function getTokens(s: string) { | |
| const tokens = new Array<Token>(); | |
| let state = 0; | |
| while (state < s.size()) { | |
| let found = false; | |
| for (const { kind, matcher } of tokenMatchers) { | |
| const [, nextState, v] = s.find(matcher, state); | |
| if (nextState !== undefined) { | |
| const value = v as string; | |
| state = nextState + 1; | |
| if (kind === "Whitespace") continue; | |
| if (kind === "Operator") | |
| tokens.push({ | |
| kind, | |
| value: Object.values(Operators).find(operator => operator.symbols.includes(value))!, | |
| }); | |
| else if (kind === "Number") { | |
| tokens.push({ kind, value }); | |
| } else if (kind === "LogFunction") tokens.push({ kind }); | |
| else if (kind === "Function") tokens.push({ kind, value: Functions.get(value)! }); | |
| else if (kind === "Constant") tokens.push({ kind: "Constant", value: Constants.get(value)! }); | |
| else tokens.push({ kind, value }); | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| tokens.push({ kind: "Erroneous", value: `No valid token exists at ${state}` }); | |
| return tokens; | |
| } | |
| } | |
| return tokens; | |
| } | |
| export function* create(s: string): Generator<Token, undefined> { | |
| let state = 0; | |
| while (state < s.size()) { | |
| let found = false; | |
| for (const { kind, matcher } of tokenMatchers) { | |
| const [, nextState, v] = s.find(matcher, state); | |
| if (nextState !== undefined) { | |
| const value = v as string; | |
| state = nextState + 1; | |
| if (kind === "Whitespace") continue; | |
| if (kind === "Operator") | |
| yield { | |
| kind, | |
| value: Object.values(Operators).find(operator => operator.symbols.includes(value))!, | |
| }; | |
| else if (kind === "Number") { | |
| yield { kind, value }; | |
| } else if (kind === "LogFunction") yield { kind }; | |
| else if (kind === "Function") yield { kind, value: Functions.get(value)! }; | |
| else if (kind === "Constant") yield { kind: "Constant", value: Constants.get(value)! }; | |
| else yield { kind, value }; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) yield { kind: "Erroneous", value: `No valid token exists at ${state}` }; | |
| } | |
| return undefined; | |
| } | |
| } | |
| const bases = { 2: "b", 8: "o", 16: "x" }; | |
| function processToken(tokenizer: Generator<Tokenizer.Token>, token: Tokenizer.Token) {} | |
| const x = { | |
| f: function*(x: number) { | |
| yield x; | |
| }, | |
| }; | |
| const r = x.f(1); | |
| for (const n of x.f(5)) { | |
| print(n); | |
| } | |
| /** Simple math engine for evaluating simple, single-result expressions. Works like a simple calculator. */ | |
| export function calculator( | |
| expressionString: string, | |
| ): | |
| | { | |
| valid: true; | |
| result: number; | |
| } | |
| | { | |
| valid: false; | |
| error: string; | |
| } { | |
| const tokenizer = Tokenizer.create(expressionString); | |
| for (let token of tokenizer as Generator<Tokenizer.Token | undefined>) { | |
| while (token) { | |
| switch (token.kind) { | |
| case "Erroneous": | |
| return { valid: false, error: token.value }; | |
| case "LogFunction": | |
| case "Comma": | |
| token = undefined; | |
| break; | |
| case "Operator": | |
| print(token.kind, token.value.symbols[0]); | |
| token = undefined; | |
| break; | |
| case "Number": { | |
| let extraToken: Tokenizer.Token | undefined; | |
| let { value } = token; | |
| let negative = false; | |
| let exponent = 0; | |
| let base: keyof typeof bases | undefined; | |
| if (value.startsWith("-")) { | |
| negative = true; | |
| value = value.slice(1); | |
| } | |
| if (value.startsWith("0")) { | |
| switch (value.slice(1, 2)) { | |
| case "B": | |
| case "b": | |
| base = 2; | |
| value = value.slice(2); | |
| break; | |
| case "O": | |
| case "o": | |
| base = 8; | |
| value = value.slice(2); | |
| break; | |
| case "X": | |
| case "x": | |
| base = 16; | |
| value = value.slice(2); | |
| break; | |
| } | |
| } | |
| if (value.endsWith("_")) { | |
| value = value.slice(0, -1); | |
| const nextResult = tokenizer.next(); | |
| if (nextResult.done || nextResult.value.kind !== "Number") { | |
| const returnValue = { | |
| valid: false, | |
| error: `Invalid number, cannot end a number (${value}) with \`_\` without a base to follow it`, | |
| result: NaN, | |
| }; | |
| return returnValue; | |
| } | |
| if (base !== undefined) { | |
| const returnValue = { | |
| valid: false, | |
| error: `Invalid number, has two base indicators: 0${bases[base]} and ${nextResult.value.value}`, | |
| result: NaN, | |
| }; | |
| return returnValue; | |
| } | |
| if (nextResult.value.value.find("^%d+$")[0] !== undefined) { | |
| base = tonumber(nextResult.value.value) as keyof typeof bases | undefined; // we're lying here, but we'll be fine :) | |
| } | |
| if (base === undefined || base < 2 || base > 36) { | |
| const returnValue = { | |
| valid: false, | |
| error: `Invalid number: base must be an integer between 2 and 36 inclusive, got ${nextResult.value.value}`, | |
| result: NaN, | |
| }; | |
| return returnValue; | |
| } | |
| } else if (value.endsWith("E") || value.endsWith("e")) { | |
| value = value.slice(0, -1); | |
| const nextResult = tokenizer.next(); | |
| if (nextResult.done || nextResult.value.kind !== "Number") { | |
| exponent = math.log10(Constants.get("e")!); // Treat as euler's number :) | |
| extraToken = nextResult.value; | |
| } else { | |
| if (nextResult.value.value.find("^%d+$")[0] === undefined) { | |
| const returnValue = { | |
| valid: false, | |
| error: "exponent on number should be a basic decimal integer", | |
| result: NaN, | |
| }; | |
| return returnValue; | |
| } | |
| exponent = tonumber(nextResult.value.value)!; | |
| } | |
| } | |
| const sections = value.split("."); | |
| const [integer, fraction] = sections as [string, string | undefined]; | |
| let number = tonumber(integer, base); | |
| if (number === undefined) { | |
| const returnValue = { | |
| valid: false, | |
| error: `invalid number: ${token.value}`, | |
| result: NaN, | |
| }; | |
| return returnValue; | |
| } | |
| if (fraction !== undefined) { | |
| const fractionalNumber = tonumber(fraction, base); | |
| if (fractionalNumber !== undefined) { | |
| number += fractionalNumber / (base || 10) ** fraction.size(); | |
| } | |
| } | |
| number *= 10 ** exponent; | |
| if (negative) { | |
| number = -number; | |
| } | |
| // TODO: number is ready to process :) | |
| token = extraToken; | |
| break; | |
| } | |
| default: | |
| print(token.kind, token.value); | |
| token = undefined; | |
| } | |
| } | |
| } | |
| return { valid: true, result: NaN }; | |
| } | |
| const pos = calculator("2 pi"); | |
| print(pos.valid, pos.valid ? pos.result : pos.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment