Skip to content

Instantly share code, notes, and snippets.

@Validark
Created May 7, 2020 07:38
Show Gist options
  • Select an option

  • Save Validark/f677cfa22cf2d67d3ff902d42ff9bc14 to your computer and use it in GitHub Desktop.

Select an option

Save Validark/f677cfa22cf2d67d3ff902d42ff9bc14 to your computer and use it in GitHub Desktop.
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