Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,55 @@
import { CalculationResult } from "./types";
import { tokenize, findOperation } from "./utils/parser";
import { tokenize, findOperation, findFunction, ParsedToken } from "./utils/parser";
import { validateNumber, validateResult, ValidationError } from "./utils/validator";

// Evaluate a single function argument: direct number (incl. negatives) or a
// sub-expression that is recursively calculated (e.g. "2 + 2" or "sqrt(16)").
function evaluateArg(arg: string): number {
const trimmed = arg.trim();
if (trimmed === "") {
throw new ValidationError("Empty function argument");
}
if (/^-?\d*\.?\d+$/.test(trimmed)) {
return validateNumber(trimmed);
}
return calculate(trimmed).value;
}

// Evaluate a parsed token into a numeric value.
function evaluateToken(token: ParsedToken): number {
if (token.type === "number") {
return validateNumber(String(token.value));
}
if (token.type === "function") {
const fn = findFunction(String(token.value));
const argValues = (token.args || []).map(evaluateArg);
return fn.fn(...argValues);
}
throw new ValidationError("Malformed expression");
}

export function calculate(expression: string): CalculationResult {
const tokens = tokenize(expression);

// Simple left-to-right evaluation (no parentheses for simplicity)
let result = validateNumber(String(tokens[0].value));
let lastOp = "start";
// Simple left-to-right evaluation (no parentheses for binary ops)
let result = evaluateToken(tokens[0]);
let lastOp =
tokens[0].type === "function" ? String(tokens[0].value) : "start";

for (let i = 1; i < tokens.length; i += 2) {
const opToken = tokens[i];
const numToken = tokens[i + 1];
const operandToken = tokens[i + 1];

if (!numToken || opToken.type !== "operator" || numToken.type !== "number") {
if (
!operandToken ||
opToken.type !== "operator" ||
(operandToken.type !== "number" && operandToken.type !== "function")
) {
throw new ValidationError("Malformed expression");
}

const op = findOperation(String(opToken.value));
const num = validateNumber(String(numToken.value));
const num = evaluateToken(operandToken);
result = op.fn(result, num);
lastOp = op.name;
}
Expand Down
3 changes: 3 additions & 0 deletions src/operations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export { add, addDescriptor } from "./add";
export { subtract, subtractDescriptor } from "./subtract";
export { multiply, multiplyDescriptor } from "./multiply";
export { divide, divideDescriptor } from "./divide";
export { sqrt, sqrtDescriptor } from "./sqrt";
export { root, rootDescriptor } from "./root";
export { log, logDescriptor } from "./log";
21 changes: 21 additions & 0 deletions src/operations/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FunctionOperationDescriptor } from "../types";
import { ValidationError } from "../utils/validator";

// log(x, base) computes log_base(x). If base is omitted, natural log is used.
export const log = (x: number, base?: number): number => {
if (x <= 0) {
throw new ValidationError("Logarithm of non-positive number");
}
if (base !== undefined) {
if (base <= 0 || base === 1) {
throw new ValidationError("Invalid logarithm base");
}
return Math.log(x) / Math.log(base);
}
return Math.log(x);
};

export const logDescriptor: FunctionOperationDescriptor = {
name: "log",
fn: log,
};
19 changes: 19 additions & 0 deletions src/operations/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FunctionOperationDescriptor } from "../types";
import { ValidationError } from "../utils/validator";

// nth root: root(x, n) computes the n-th root of x (ⁿ√x)
export const root = (x: number, n: number): number => {
if (n === 0) {
throw new ValidationError("Root degree cannot be zero");
}
if (x < 0 && n % 2 === 0) {
throw new ValidationError("Even root of negative number");
}
const sign = x < 0 ? -1 : 1;
return sign * Math.pow(Math.abs(x), 1 / n);
};

export const rootDescriptor: FunctionOperationDescriptor = {
name: "root",
fn: root,
};
14 changes: 14 additions & 0 deletions src/operations/sqrt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FunctionOperationDescriptor } from "../types";
import { ValidationError } from "../utils/validator";

export const sqrt = (x: number): number => {
if (x < 0) {
throw new ValidationError("Square root of negative number");
}
return Math.sqrt(x);
};

export const sqrtDescriptor: FunctionOperationDescriptor = {
name: "sqrt",
fn: sqrt,
};
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ export interface CalculationResult {
}

export type BinaryOperation = (a: number, b: number) => number;
export type VariadicOperation = (...args: number[]) => number;

export interface OperationDescriptor {
name: string;
symbol: string;
fn: BinaryOperation;
precedence: number;
}

export interface FunctionOperationDescriptor {
name: string;
fn: VariadicOperation;
}
97 changes: 85 additions & 12 deletions src/utils/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { addDescriptor } from "../operations/add";
import { subtractDescriptor } from "../operations/subtract";
import { multiplyDescriptor } from "../operations/multiply";
import { divideDescriptor } from "../operations/divide";
import { OperationDescriptor } from "../types";
import { sqrtDescriptor } from "../operations/sqrt";
import { rootDescriptor } from "../operations/root";
import { logDescriptor } from "../operations/log";
import { OperationDescriptor, FunctionOperationDescriptor } from "../types";
import { ValidationError } from "./validator";

const operations: OperationDescriptor[] = [
Expand All @@ -12,32 +15,94 @@ const operations: OperationDescriptor[] = [
divideDescriptor,
];

const functions: FunctionOperationDescriptor[] = [
sqrtDescriptor,
rootDescriptor,
logDescriptor,
];

export interface ParsedToken {
type: "number" | "operator";
type: "number" | "operator" | "function";
value: number | string;
args?: string[];
}

export function tokenize(input: string): ParsedToken[] {
const trimmed = input.replace(/\s+/g, "");
const tokens: ParsedToken[] = [];
let current = "";
let i = 0;

const flushNumber = () => {
if (current) {
tokens.push({ type: "number", value: Number(current) });
current = "";
}
};

while (i < trimmed.length) {
const ch = trimmed[i];

for (const ch of trimmed) {
// Binary operator
if ("+-*/".includes(ch)) {
if (current) {
tokens.push({ type: "number", value: Number(current) });
current = "";
}
flushNumber();
tokens.push({ type: "operator", value: ch });
} else {
current += ch;
i++;
continue;
}

// Letter: start of a function name (e.g. sqrt, root, log)
if (/[a-zA-Z]/.test(ch)) {
flushNumber();
let name = "";
while (i < trimmed.length && /[a-zA-Z]/.test(trimmed[i])) {
name += trimmed[i];
i++;
}
if (trimmed[i] !== "(") {
throw new ValidationError(`Expected '(' after function name: ${name}`);
}
i++; // consume '('

// Collect comma-separated arguments, respecting nested parentheses
const args: string[] = [];
let arg = "";
let depth = 1;
while (i < trimmed.length && depth > 0) {
const c = trimmed[i];
if (c === "(") {
depth++;
arg += c;
} else if (c === ")") {
depth--;
if (depth === 0) {
if (arg !== "") args.push(arg);
break;
}
arg += c;
} else if (c === "," && depth === 1) {
args.push(arg);
arg = "";
} else {
arg += c;
}
i++;
}
if (depth !== 0) {
throw new ValidationError(`Unclosed parenthesis in function: ${name}`);
}
i++; // consume ')'
tokens.push({ type: "function", value: name, args });
continue;
}
}

if (current) {
tokens.push({ type: "number", value: Number(current) });
// Digit or decimal point
current += ch;
i++;
}

flushNumber();

if (tokens.length === 0) {
throw new ValidationError("Empty expression");
}
Expand All @@ -52,3 +117,11 @@ export function findOperation(symbol: string): OperationDescriptor {
}
return op;
}

export function findFunction(name: string): FunctionOperationDescriptor {
const fn = functions.find((f) => f.name === name);
if (!fn) {
throw new ValidationError(`Unknown function: ${name}`);
}
return fn;
}