From 4ae186b2530c46e7b14df675cf103d64689c58d7 Mon Sep 17 00:00:00 2001 From: NLazyCat Date: Sat, 18 Jul 2026 12:37:57 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BC=80=E5=B9=B3?= =?UTF-8?q?=E6=96=B9=E3=80=81=E5=BC=80=E6=A0=B9=E3=80=81=E5=8F=96=E5=AF=B9?= =?UTF-8?q?=E6=95=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- src/index.ts | 45 ++++++++++++++++--- src/operations/index.ts | 3 ++ src/operations/log.ts | 21 +++++++++ src/operations/root.ts | 19 ++++++++ src/operations/sqrt.ts | 14 ++++++ src/types.ts | 6 +++ src/utils/parser.ts | 97 ++++++++++++++++++++++++++++++++++++----- 7 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 src/operations/log.ts create mode 100644 src/operations/root.ts create mode 100644 src/operations/sqrt.ts diff --git a/src/index.ts b/src/index.ts index 4d041f5..959e516 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; } diff --git a/src/operations/index.ts b/src/operations/index.ts index 1590009..4db2548 100644 --- a/src/operations/index.ts +++ b/src/operations/index.ts @@ -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"; diff --git a/src/operations/log.ts b/src/operations/log.ts new file mode 100644 index 0000000..afb1f0c --- /dev/null +++ b/src/operations/log.ts @@ -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, +}; diff --git a/src/operations/root.ts b/src/operations/root.ts new file mode 100644 index 0000000..7f6ddf5 --- /dev/null +++ b/src/operations/root.ts @@ -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, +}; diff --git a/src/operations/sqrt.ts b/src/operations/sqrt.ts new file mode 100644 index 0000000..22bc536 --- /dev/null +++ b/src/operations/sqrt.ts @@ -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, +}; diff --git a/src/types.ts b/src/types.ts index c83c067..e49b0fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export interface CalculationResult { } export type BinaryOperation = (a: number, b: number) => number; +export type VariadicOperation = (...args: number[]) => number; export interface OperationDescriptor { name: string; @@ -12,3 +13,8 @@ export interface OperationDescriptor { fn: BinaryOperation; precedence: number; } + +export interface FunctionOperationDescriptor { + name: string; + fn: VariadicOperation; +} diff --git a/src/utils/parser.ts b/src/utils/parser.ts index aef5794..5492bff 100644 --- a/src/utils/parser.ts +++ b/src/utils/parser.ts @@ -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[] = [ @@ -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"); } @@ -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; +}