Last active
January 24, 2026 03:00
-
-
Save NickChristensen/d51c3b01e84a4d4a3e60f810808e1b0a to your computer and use it in GitHub Desktop.
Normalize Contacts phone numbers to E.164 (dry-run supported)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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