Skip to content

Instantly share code, notes, and snippets.

@NickChristensen
Last active January 24, 2026 03:00
Show Gist options
  • Select an option

  • Save NickChristensen/d51c3b01e84a4d4a3e60f810808e1b0a to your computer and use it in GitHub Desktop.

Select an option

Save NickChristensen/d51c3b01e84a4d4a3e60f810808e1b0a to your computer and use it in GitHub Desktop.
Normalize Contacts phone numbers to E.164 (dry-run supported)
property dryRun : true
property defaultCountryCode : "1"
property minDigits : 10
on isDigit(ch)
return (ch ≥ "0" and ch ≤ "9")
end isDigit
on digitsOnly(s)
set out to ""
repeat with i from 1 to length of s
set ch to text i thru i of s
if my isDigit(ch) then set out to out & ch
end repeat
return out
end digitsOnly
on hasExtension(raw)
ignoring case
if raw contains "ext" then return true
if raw contains "extension" then return true
end ignoring
set rawText to raw as string
set len to length of rawText
repeat with i from 1 to len
set ch to text i thru i of rawText
if ch is "x" or ch is "X" then
set j to i + 1
repeat while j ≤ len
set nextChar to text j thru j of rawText
if nextChar is " " or nextChar is "." or nextChar is "-" then
set j to j + 1
else
if my isDigit(nextChar) then return true
exit repeat
end if
end repeat
end if
end repeat
return false
end hasExtension
on normalizePhone(rawValue)
set raw to rawValue as string
if raw is "" then return {raw, "empty"}
if my hasExtension(raw) then error "Extension detected in phone value: " & raw
set hadPlus to raw begins with "+"
set digits to my digitsOnly(raw)
if digits is "" then return {raw, "empty"}
if (length of digits) < minDigits then return {raw, "shortcode"}
if hadPlus then
set candidate to "+" & digits
if candidate is raw then return {raw, "unchanged"}
return {candidate, "normalized"}
end if
if (length of digits) = 10 then
set candidate to "+" & defaultCountryCode & digits
if candidate is raw then return {raw, "unchanged"}
return {candidate, "normalized"}
end if
if (length of digits) = 11 and digits begins with defaultCountryCode then
set candidate to "+" & digits
if candidate is raw then return {raw, "unchanged"}
return {candidate, "normalized"}
end if
return {raw, "unchanged"}
end normalizePhone
on joinLines(linesList)
set oldTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to linefeed
set out to linesList as text
set AppleScript's text item delimiters to oldTID
return out
end joinLines
set logs to {}
set totalPhones to 0
set normalizedCount to 0
set shortcodeCount to 0
set unchangedCount to 0
set emptyCount to 0
if dryRun then set end of logs to "Starting dry run..."
tell application "Contacts"
set peopleList to people
repeat with p in peopleList
set displayName to name of p
if displayName is missing value then set displayName to "(no name)"
set phoneList to phones of p
repeat with ph in phoneList
set oldValue to value of ph
set norm to my normalizePhone(oldValue)
set newValue to item 1 of norm
set status to item 2 of norm
set totalPhones to totalPhones + 1
if status is "normalized" then
set normalizedCount to normalizedCount + 1
else if status is "shortcode" then
set shortcodeCount to shortcodeCount + 1
else if status is "unchanged" then
set unchangedCount to unchangedCount + 1
else if status is "empty" then
set emptyCount to emptyCount + 1
end if
if dryRun then
set end of logs to (displayName & ": " & oldValue & " => " & newValue & " [" & status & "]")
else
if status is "shortcode" then
-- no-op
else if newValue is not oldValue then
set value of ph to newValue
end if
end if
end repeat
end repeat
end tell
set summary to "Done. total=" & totalPhones & " normalized=" & normalizedCount & " shortcode=" & shortcodeCount & " unchanged=" & unchangedCount & " empty=" & emptyCount
if not dryRun then
tell application "Contacts"
if unsaved then save
end tell
end if
if dryRun then
set end of logs to summary
return my joinLines(logs)
else
return summary
end if
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment