Conversions & Utilities

Calculate

Evaluate a selected mathematical expression locally.

OfficialOn-device

Download action

What Calculate does

Uses a purpose-built parser rather than eval. Supports arithmetic, parentheses, right-associative powers, implicit multiplication, percentages, constants, and common functions, with finite results rounded to 14 significant digits. A trailing equals sign appends the result.

CategoryConversions & Utilities
Action typeJavaScript
ResultReplace or copy text
InternetNot required

Action and outcome

Calculate example
ActionOutcome
CalculateSelected text: 100 / 4 =100 / 4 =25

Use Calculate

Follow these steps to evaluate a selected mathematical expression locally with ActionClip.

  1. Select the text you want to use.
  2. Choose Calculate from ActionClip.
  3. Review the result, then replace the selection or copy the new text.

Requirements and privacy

  • Before you start: No setup required.
  • Your selected text: Runs locally in ActionClip’s JavaScript sandbox. Selected text does not leave your Mac.

Action configuration

The definition below is included so you can inspect how the action works before downloading it.

JavaScript definition
function run(selected_text) {
  const original = selected_text.trim();
  if (!original) throw new Error("Enter a mathematical expression.");
  if (original.length > 1000) throw new Error("Expressions are limited to 1,000 characters.");

  const appendsResult = /=\s*$/.test(original);
  let expression = appendsResult ? original.replace(/=\s*$/, "").trim() : original;
  if (!expression) throw new Error("Enter an expression before the equals sign.");

  // Decimal commas are accepted for arithmetic-only input. Function argument
  // commas remain separators, so min(1, 2) is never rewritten as a decimal.
  if (!/[A-Za-z_]/.test(expression) && !expression.includes(".") && /\d,\d/.test(expression)) {
    expression = expression.replace(/(\d),(?=\d)/g, "$1.");
  }

  const tokens = [];
  let index = 0;
  while (index < expression.length) {
    const character = expression[index];
    if (/\s/.test(character)) {
      index += 1;
      continue;
    }

    if (/\d/.test(character) || (character === "." && /\d/.test(expression[index + 1] || ""))) {
      const start = index;
      let sawDot = false;
      if (character === ".") {
        sawDot = true;
        index += 1;
      }
      while (/\d/.test(expression[index] || "")) index += 1;
      if (!sawDot && expression[index] === ".") {
        sawDot = true;
        index += 1;
        while (/\d/.test(expression[index] || "")) index += 1;
      }
      if (/[eE]/.test(expression[index] || "")) {
        const exponentStart = index;
        index += 1;
        if (/[+-]/.test(expression[index] || "")) index += 1;
        const digitsStart = index;
        while (/\d/.test(expression[index] || "")) index += 1;
        if (digitsStart === index) {
          throw new Error("Invalid exponent near position " + (exponentStart + 1) + ".");
        }
      }
      const rawNumber = expression.slice(start, index);
      const numeric = Number(rawNumber);
      if (!Number.isFinite(numeric)) throw new Error("Invalid number: " + rawNumber + ".");
      tokens.push({ type: "number", value: numeric });
      continue;
    }

    if (/[A-Za-z_]/.test(character)) {
      const start = index;
      index += 1;
      while (/[A-Za-z0-9_]/.test(expression[index] || "")) index += 1;
      tokens.push({ type: "identifier", value: expression.slice(start, index).toLowerCase() });
      continue;
    }

    if ("+-*/^(),%".includes(character)) {
      tokens.push({ type: character, value: character });
      index += 1;
      continue;
    }
    throw new Error("Unsupported character “" + character + "” at position " + (index + 1) + ".");
  }
  tokens.push({ type: "end", value: "" });

  let position = 0;
  const current = () => tokens[position];
  const match = (type) => {
    if (current().type !== type) return false;
    position += 1;
    return true;
  };
  const expect = (type, label) => {
    if (!match(type)) throw new Error("Expected " + label + ".");
  };
  const finite = (value) => {
    if (!Number.isFinite(value)) throw new Error("The result is not a finite number.");
    return value;
  };

  const constants = { pi: Math.PI, e: Math.E, tau: Math.PI * 2 };
  const functions = {
    sqrt: (values) => values.length === 1 ? Math.sqrt(values[0]) : NaN,
    abs: (values) => values.length === 1 ? Math.abs(values[0]) : NaN,
    sin: (values) => values.length === 1 ? Math.sin(values[0]) : NaN,
    cos: (values) => values.length === 1 ? Math.cos(values[0]) : NaN,
    tan: (values) => values.length === 1 ? Math.tan(values[0]) : NaN,
    asin: (values) => values.length === 1 ? Math.asin(values[0]) : NaN,
    acos: (values) => values.length === 1 ? Math.acos(values[0]) : NaN,
    atan: (values) => values.length === 1 ? Math.atan(values[0]) : NaN,
    ln: (values) => values.length === 1 ? Math.log(values[0]) : NaN,
    log: (values) => values.length === 1 ? Math.log(values[0]) : NaN,
    log10: (values) => values.length === 1 ? Math.log(values[0]) / Math.LN10 : NaN,
    exp: (values) => values.length === 1 ? Math.exp(values[0]) : NaN,
    floor: (values) => values.length === 1 ? Math.floor(values[0]) : NaN,
    ceil: (values) => values.length === 1 ? Math.ceil(values[0]) : NaN,
    round: (values) => {
      if (values.length === 1) return Math.round(values[0]);
      if (values.length !== 2 || !Number.isInteger(values[1]) || Math.abs(values[1]) > 12) return NaN;
      const scale = Math.pow(10, values[1]);
      return Math.round((values[0] + Number.EPSILON) * scale) / scale;
    },
    min: (values) => values.length ? Math.min(...values) : NaN,
    max: (values) => values.length ? Math.max(...values) : NaN,
    pow: (values) => values.length === 2 ? Math.pow(values[0], values[1]) : NaN,
    mod: (values) => values.length === 2 ? values[0] % values[1] : NaN
  };

  let parseExpression;
  let parseUnary;
  const parsePrimary = () => {
    if (current().type === "number") {
      const value = current().value;
      position += 1;
      return value;
    }
    if (current().type === "identifier") {
      const name = current().value;
      position += 1;
      if (match("(")) {
        const values = [];
        if (!match(")")) {
          do { values.push(parseExpression()); } while (match(","));
          expect(")", "a closing parenthesis");
        }
        const callable = functions[name];
        if (!callable) throw new Error("Unknown function: " + name + ".");
        return finite(callable(values));
      }
      if (Object.prototype.hasOwnProperty.call(constants, name)) return constants[name];
      throw new Error("Unknown constant: " + name + ".");
    }
    if (match("(")) {
      const value = parseExpression();
      expect(")", "a closing parenthesis");
      return value;
    }
    throw new Error("Expected a number, constant, function, or opening parenthesis.");
  };

  const parsePostfix = () => {
    let value = parsePrimary();
    while (match("%")) value /= 100;
    return value;
  };

  const parsePower = () => {
    const left = parsePostfix();
    if (match("^")) return finite(Math.pow(left, parseUnary()));
    return left;
  };

  parseUnary = () => {
    if (match("+")) return parseUnary();
    if (match("-")) return -parseUnary();
    return parsePower();
  };

  const startsImplicitFactor = () => ["number", "identifier", "("].includes(current().type);
  const parseProduct = () => {
    let value = parseUnary();
    while (true) {
      if (match("*")) value = finite(value * parseUnary());
      else if (match("/")) {
        const divisor = parseUnary();
        if (divisor === 0) throw new Error("Division by zero is not supported.");
        value = finite(value / divisor);
      } else if (startsImplicitFactor()) {
        value = finite(value * parseUnary());
      } else break;
    }
    return value;
  };

  parseExpression = () => {
    let value = parseProduct();
    while (true) {
      if (match("+")) value = finite(value + parseProduct());
      else if (match("-")) value = finite(value - parseProduct());
      else break;
    }
    return value;
  };

  const result = finite(parseExpression());
  if (current().type !== "end") {
    throw new Error("Unexpected token “" + current().value + "”.");
  }

  const rounded = Number(result.toPrecision(14));
  const formatted = Object.is(rounded, -0) ? "0" : String(rounded);
  return appendsResult ? original + formatted : formatted;
}