---
title: "Calculate action for ActionClip"
canonical_url: "https://actionclip.app/actions/calculate"
document_type: "standalone_action"
official: true
action_count: 1
catalog_schema_version: 1
catalog_revision: "2026-08-21.1"
catalog_published_at: "2026-08-20T18:50:00Z"
---

# Calculate action for ActionClip

Evaluate a selected mathematical expression locally.

## What it 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.

## Action details

| Field | Value |
| --- | --- |
| Category | Conversions & Utilities |
| Action type | JavaScript |
| Result | Replace or copy text |
| Internet | Not required |

## Action and outcome

| Action | Outcome |
| --- | --- |
| **Calculate** — Selected text: 100 / 4 = | 100 / 4 =25 |

## How 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

No setup required.

## Privacy

Runs locally in ActionClip’s JavaScript sandbox. Selected text does not leave your Mac.

## Action configuration

````javascript
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;
}
````

## Links

- [HTML listing](https://actionclip.app/actions/calculate)
- [Download action definition](https://actionclip.app/actions/calculate.actionclip)
- [Browse Conversions & Utilities actions](https://actionclip.app/marketplace#marketplace-conversions)
- [All Marketplace listings](https://actionclip.app/marketplace)
