Extraction

Extract Phone Numbers

Extract unique phone numbers from selected text.

OfficialOn-device

Download action

What Extract Phone Numbers does

Finds common local and international phone-number formats with optional country codes and extensions, preserves readable formatting, removes equivalent duplicates, and filters obvious dates, IP addresses, postal codes, government-ID patterns, and malformed or overlong numbers.

CategoryExtraction
Action typeJavaScript
Default resultCopy the result
InternetNot required

Action and outcome

Extract Phone Numbers example
ActionOutcome
Extract Phone NumbersSelected text: Call +1 (415) 555-2671 or +44 20 7946 0958 ext. 42.+1 (415) 555-2671 +44 20 7946 0958 ext. 42

Use Extract Phone Numbers

Follow these steps to extract unique phone numbers from selected text with ActionClip.

  1. Select the text you want to use.
  2. Choose Extract Phone Numbers from ActionClip.
  3. Review the extracted result, then copy it where you need it.

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 script

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

JavaScript definition
function run(selected_text) {
  const horizontalSpace = "[\\t \\u00A0\\u202F]";
  const candidatePattern = new RegExp(
    "(?:(?:\\+|00)" + horizontalSpace + "*(?:"
      + "\\(" + horizontalSpace + "*\\d{1,4}" + horizontalSpace + "*\\)|\\d)"
      + "|(?:\\(" + horizontalSpace + "*\\d{1,4}" + horizontalSpace + "*\\)|\\d))"
      + "[\\d\\t \\u00A0\\u202F().\\/-]{3,}\\d"
      + "(?:" + horizontalSpace + "*(?:(?:ext(?:ension)?)\\.?|x|#)"
      + horizontalSpace + "*\\d{1,6})?",
    "giu"
  );
  const extensionPattern = /[\t \u00A0\u202F]*(?:(?:ext(?:ension)?)\.?|x|#)[\t \u00A0\u202F]*(\d{1,6})$/iu;

  function hasSafeBoundaries(match) {
    const before = match.index > 0 ? selected_text[match.index - 1] : "";
    const afterIndex = match.index + match[0].length;
    const after = afterIndex < selected_text.length ? selected_text[afterIndex] : "";
    if ((before === "(") !== (after === ")")) return false;
    return !/[\p{L}\p{N}_]/u.test(before) && !/[\p{L}\p{N}_]/u.test(after);
  }

  function hasBalancedPhoneParentheses(value) {
    let depth = 0;
    for (const character of value) {
      if (character === "(") {
        depth += 1;
        if (depth > 1) return false;
      } else if (character === ")") {
        depth -= 1;
        if (depth < 0) return false;
      }
    }
    if (depth !== 0) return false;
    const withoutValidGroups = value.replace(/\([\t \u00A0\u202F]*\d{1,4}[\t \u00A0\u202F]*\)/g, "");
    return !/[()]/.test(withoutValidGroups);
  }

  function isObviousNonPhone(value) {
    const compact = value.replace(/[\t \u00A0\u202F]/g, "");
    if (/^(?:19|20)\d{2}[./-]\d{1,2}[./-]\d{1,2}$/.test(compact)) return true;
    if (/^\d{1,2}[./-]\d{1,2}[./-](?:19|20)\d{2}$/.test(compact)) return true;
    if (/^\d{3}-\d{2}-\d{4}$/.test(compact)) return true;
    if (/^\d{5}-\d{4}$/.test(compact)) return true;
    if (/^97[89](?:[-.]\d+){3,5}$/.test(compact)) return true;

    const ipv4 = compact.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
    return Boolean(ipv4 && ipv4.slice(1).every((part) => Number(part) <= 255));
  }

  function parseCandidate(match) {
    if (!hasSafeBoundaries(match)) return null;

    const readable = match[0]
      .replace(/[\u00A0\u202F]/g, " ")
      .replace(/[\t ]+/g, " ")
      .trim();
    const extensionMatch = readable.match(extensionPattern);
    const extension = extensionMatch ? extensionMatch[1] : "";
    const core = extensionMatch
      ? readable.slice(0, extensionMatch.index).trim()
      : readable;

    if (!hasBalancedPhoneParentheses(core) || isObviousNonPhone(core)) return null;

    const digits = core.replace(/\D/g, "");
    const usesDialOutPrefix = /^00/.test(core.replace(/[\t ]/g, ""));
    const phoneDigits = usesDialOutPrefix ? digits.slice(2) : digits;
    if (phoneDigits.length < 7 || phoneDigits.length > 15) return null;
    if (/^(\d)\1+$/.test(phoneDigits)) return null;

    const groups = core
      .replace(/^\+/, "")
      .replace(/[()]/g, " ")
      .trim()
      .split(/[\t ./-]+/)
      .filter(Boolean);
    if (groups.length > 1 && groups.some((group) => group.length > 6)) return null;
    if (groups.length > 6) return null;

    const canonicalCore = usesDialOutPrefix
      ? "+" + phoneDigits
      : (core.trim().startsWith("+") ? "+" : "") + phoneDigits;
    return {
      value: readable,
      key: canonicalCore + (extension ? "x" + extension : "")
    };
  }

  const seen = new Set();
  const phoneNumbers = [];
  for (const match of selected_text.matchAll(candidatePattern)) {
    const parsed = parseCandidate(match);
    if (!parsed || seen.has(parsed.key)) continue;
    seen.add(parsed.key);
    phoneNumbers.push(parsed.value);
  }

  return phoneNumbers.length ? phoneNumbers.join("\n") : "No phone numbers found.";
}