Developing a Calculator Skill for OpenClaw

Clawpedia · For Humans

Learn skill development fundamentals by building a fully functional calculator skill for OpenClaw.

Developing a Calculator Skill for OpenClaw

Building a calculator skill is one of the best beginner projects for learning OpenClaw skill development. It covers all the fundamentals — parameter handling, input validation, output formatting, and error handling — in a simple, testable package.

---

What We Will Build

A skill that:

FeatureDescription
Basic mathAddition, subtraction, multiplication, division
Advanced mathPowers, roots, percentages, factorials
Unit conversionLength, weight, temperature, currency
Expression parsingEvaluate full math expressions
HistoryRemember recent calculations

---

Step 1: Create the Skill Structure


mkdir -p ~/.openclaw/skills/calculator
cd ~/.openclaw/skills/calculator

Manifest


{
  "name": "calculator",
  "version": "1.0.0",
  "description": "Mathematical calculations and unit conversions",
  "author": "Your Name",
  "triggers": ["calculate", "calc", "math", "convert"],
  "parameters": {
    "expression": {
      "type": "string",
      "description": "Math expression to evaluate",
      "required": true
    }
  }
}

---

Step 2: Build the Math Engine


// math-engine.js

// Safe math expression evaluator (no eval!)
export function evaluateExpression(expr) {
  // Tokenize
  const tokens = tokenize(expr);
  
  // Parse and evaluate using recursive descent
  const result = parseExpression(tokens, 0);
  
  return result.value;
}

function tokenize(expr) {
  const tokens = [];
  let i = 0;
  const cleaned = expr.replace(/\s+/g, "");
  
  while (i < cleaned.length) {
    if (/[0-9.]/.test(cleaned[i])) {
      let num = "";
      while (i < cleaned.length && /[0-9.]/.test(cleaned[i])) {
        num += cleaned[i++];
      }
      tokens.push({ type: "number", value: parseFloat(num) });
    } else if ("+-*/^%()!".includes(cleaned[i])) {
      tokens.push({ type: "operator", value: cleaned[i++] });
    } else {
      // Function names (sqrt, sin, cos, etc.)
      let name = "";
      while (i < cleaned.length && /[a-z]/i.test(cleaned[i])) {
        name += cleaned[i++];
      }
      tokens.push({ type: "function", value: name.toLowerCase() });
    }
  }
  
  return tokens;
}

// Supported functions
const FUNCTIONS = {
  sqrt: Math.sqrt,
  sin: Math.sin,
  cos: Math.cos,
  tan: Math.tan,
  abs: Math.abs,
  log: Math.log10,
  ln: Math.log,
  round: Math.round,
  floor: Math.floor,
  ceil: Math.ceil
};

function parseExpression(tokens, pos) {
  // Implementation of recursive descent parser
  // Handles operator precedence: +- < */ < ^ < unary < functions
  let left = parseTerm(tokens, pos);
  
  while (left.pos < tokens.length && 
         tokens[left.pos]?.type === "operator" && 
         "+-".includes(tokens[left.pos].value)) {
    const op = tokens[left.pos].value;
    const right = parseTerm(tokens, left.pos + 1);
    left = {
      value: op === "+" ? left.value + right.value : left.value - right.value,
      pos: right.pos
    };
  }
  
  return left;
}

function parseTerm(tokens, pos) {
  let left = parseFactor(tokens, pos);
  
  while (left.pos < tokens.length && 
         tokens[left.pos]?.type === "operator" && 
         "*/".includes(tokens[left.pos].value)) {
    const op = tokens[left.pos].value;
    const right = parseFactor(tokens, left.pos + 1);
    if (op === "/" && right.value === 0) throw new Error("Division by zero");
    left = {
      value: op === "*" ? left.value * right.value : left.value / right.value,
      pos: right.pos
    };
  }
  
  return left;
}

function parseFactor(tokens, pos) {
  if (tokens[pos]?.type === "number") {
    return { value: tokens[pos].value, pos: pos + 1 };
  }
  if (tokens[pos]?.type === "operator" && tokens[pos].value === "(") {
    const inner = parseExpression(tokens, pos + 1);
    return { value: inner.value, pos: inner.pos + 1 }; // skip )
  }
  if (tokens[pos]?.type === "function") {
    const fn = FUNCTIONS[tokens[pos].value];
    if (!fn) throw new Error(`Unknown function: ${tokens[pos].value}`);
    const arg = parseFactor(tokens, pos + 1);
    return { value: fn(arg.value), pos: arg.pos };
  }
  throw new Error(`Unexpected token at position ${pos}`);
}

---

Step 3: Add Unit Conversion


// conversions.js

const CONVERSIONS = {
  // Length
  km_to_miles: (v) => v * 0.621371,
  miles_to_km: (v) => v * 1.60934,
  m_to_feet: (v) => v * 3.28084,
  feet_to_m: (v) => v / 3.28084,
  cm_to_inches: (v) => v / 2.54,
  inches_to_cm: (v) => v * 2.54,
  
  // Weight
  kg_to_lbs: (v) => v * 2.20462,
  lbs_to_kg: (v) => v / 2.20462,
  g_to_oz: (v) => v / 28.3495,
  oz_to_g: (v) => v * 28.3495,
  
  // Temperature
  c_to_f: (v) => v * 9/5 + 32,
  f_to_c: (v) => (v - 32) * 5/9,
  c_to_k: (v) => v + 273.15,
  k_to_c: (v) => v - 273.15,
  
  // Data
  gb_to_mb: (v) => v * 1024,
  mb_to_gb: (v) => v / 1024,
  tb_to_gb: (v) => v * 1024,
};

export function convert(value, from, to) {
  const key = `${from.toLowerCase()}_to_${to.toLowerCase()}`;
  const fn = CONVERSIONS[key];
  
  if (!fn) {
    throw new Error(`Unknown conversion: ${from} to ${to}`);
  }
  
  return {
    input: `${value} ${from}`,
    output: `${fn(value).toFixed(4)} ${to}`,
    formula: key.replace(/_/g, " ")
  };
}

export function listConversions() {
  return Object.keys(CONVERSIONS).map(key => {
    const [from, , to] = key.split("_");
    return { from: from.toUpperCase(), to: to.toUpperCase() };
  });
}

---

Step 4: Build the Main Skill


// index.js
import { evaluateExpression } from "./math-engine.js";
import { convert, listConversions } from "./conversions.js";

export default {
  name: "calculator",
  description: "Mathematical calculations and unit conversions",
  
  execute: async (context) => {
    const input = context.parameters.expression;
    
    // Detect conversion requests
    const convMatch = input.match(
      /(\d+\.?\d*)\s*(\w+)\s+(?:to|in|as)\s+(\w+)/i
    );
    
    if (convMatch) {
      try {
        const result = convert(
          parseFloat(convMatch[1]),
          convMatch[2],
          convMatch[3]
        );
        return `🔄 **Conversion**\n${result.input} = **${result.output}**`;
      } catch (e) {
        return `❌ ${e.message}\n\nAvailable conversions:\n` +
          listConversions().map(c => `  ${c.from} ↔ ${c.to}`).join("\n");
      }
    }
    
    // Evaluate math expression
    try {
      const result = evaluateExpression(input);
      const formatted = Number.isInteger(result) 
        ? result.toString() 
        : result.toFixed(6).replace(/\.?0+$/, "");
      
      return `🧮 **Result**\n\`${input}\` = **${formatted}**`;
    } catch (e) {
      return `❌ Could not evaluate: ${e.message}\n\n` +
        `Try:\n` +
        `• Math: \`2 + 3 * 4\`, \`sqrt(144)\`, \`2^10\`\n` +
        `• Convert: \`100 km to miles\`, \`72 F to C\``;
    }
  }
};

---

Step 5: Add Tests


// test.js
import { evaluateExpression } from "./math-engine.js";
import { convert } from "./conversions.js";

const tests = [
  // Basic math
  ["2 + 3", 5],
  ["10 - 4", 6],
  ["3 * 7", 21],
  ["20 / 4", 5],
  
  // Order of operations
  ["2 + 3 * 4", 14],
  ["(2 + 3) * 4", 20],
  
  // Functions
  ["sqrt(144)", 12],
  ["abs(-5)", 5],
];

let passed = 0;
for (const [expr, expected] of tests) {
  const result = evaluateExpression(expr);
  const ok = Math.abs(result - expected) < 0.0001;
  console.log(`${ok ? "✅" : "❌"} ${expr} = ${result} (expected ${expected})`);
  if (ok) passed++;
}

console.log(`\n${passed}/${tests.length} tests passed`);

Run tests:


node test.js

---

Step 6: Install and Use


openclaw skill install ~/.openclaw/skills/calculator

# Test
openclaw chat "Calculate 2^10"
openclaw chat "Convert 100 km to miles"
openclaw chat "What is sqrt(2) * 3.14?"

---

Troubleshooting

"Unknown function" Error

Only supported functions work: sqrt, sin, cos, tan, abs, log, ln, round, floor, ceil. Add more in the FUNCTIONS object.

Floating Point Precision

JavaScript floating point can produce results like 0.1 + 0.2 = 0.30000000000000004. The skill rounds output to 6 decimal places to minimize this.

Complex Expressions Fail

The parser handles standard math notation. For very complex expressions, extend the parser or use a library like math.js.

---

Summary

The calculator skill demonstrates all fundamental skill development patterns: input parsing, business logic, error handling, and formatted output. It is a great foundation for more complex skills and a practical everyday tool.

Related Articles