Last active
February 22, 2022 16:16
-
-
Save joepuzzo/677f68b29e18ac7500316f2a6a8497e8 to your computer and use it in GitHub Desktop.
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
const fs = require('fs'); | |
// Read in googles metadata json file | |
const input = fs.readFileSync(__dirname + '/metadata.json'); | |
const parsedInput = JSON.parse( input.toString() ); | |
// For storing our full output | |
const output = {}; | |
// Just formats | |
const phoneFormats = {}; | |
// Just examples | |
const examples = {}; | |
// Just patterns | |
const patterns = {}; | |
/** ------------------------------------------------------------ | |
* Given a format and pattern from googles meta json file, generate a format array | |
* | |
* formatExample = ($1) $2-$3" | |
* patternExample = (\\d{3})(\\d{3})(\\d{4,8}) | |
*/ | |
const generateFormatArray = ({ format, pattern }) => { | |
// Create match array | |
// "{4,8}" ---> 8 | |
// "{4}"" ---> 4 | |
// "(\\d)" ---> 1 | |
// pattern: "(\\d)(\\d{3})(\\d{3})(\\d{4,8})" | |
// nArray: [ 1, 3, 3, 8 ] | |
const nArray = pattern.match(/(\(\\d\))|(,\d\d*)|({\d+})/g).map( n => n.replace(/\\d/, '1').replace(/[^\d]/g, '')); | |
// PATTERN (\d{2})(\d{3})(\d{3}) | |
// SECTIONS [ [ '#', '#' ], [ '#', '#', '#' ], [ '#', '#', '#' ] ] | |
const sections = nArray.map( n => Array(+n).fill("#") ) | |
const formatter = []; | |
// Example ($1) $2-$3" ----> 3 | |
const nSections = format.match(/\$\d/g).length; | |
// Example "($1) $2-$3" | |
// Split: [ '(', ') ', '-', '' ] | |
const parts = format.split(/\$\d/); | |
// Itteration example | |
// 1: ['(', ["\\d", "\\d", "\\d"] ] | |
// 2: ['(', ["\\d", "\\d", "\\d"], ')', ["\\d", "\\d", "\\d"] ] | |
// 3: ['(', ["\\d", "\\d", "\\d"], ')', ["\\d", "\\d", "\\d"], '-', ["\\d", "\\d", "\\d", "\\d"] ] | |
for(let i = 0; i < nSections; i++ ){ | |
// push prefix ( | |
// example1: parts[i] --> '(' --> ['('] | |
// example2: parts[i] --> ') ' --> [ ')', ' ' ] | |
formatter.push(parts[i].split('')) | |
// push that section ["\\d", "\\d", "\\d"] | |
formatter.push(sections[i]); | |
} | |
// example "(###) ###-####" | |
return formatter.flat().join(''); | |
} | |
/** ------------------------------------------------------------ | |
* Generates formatters from googles meta formats | |
* | |
* Example input: | |
* | |
* country: "US" | |
* | |
* "formats": [ | |
* { | |
* "pattern": "(\\d{3})(\\d{3})(\\d{4})", | |
* "leading_digits_patterns": [ | |
* "[2-9]" | |
* ], | |
* "national_prefix_is_optional_when_formatting": true, | |
* "format": "($1) $2-$3", | |
* "international_format": "$1-$2-$3" | |
* } | |
* ], | |
* | |
* Example output: | |
* [ | |
* { | |
* leadingDigitsPattern: '[2-9]', | |
* formatter: '(###) ###-####' | |
* } | |
* ] | |
*/ | |
const generateFormatter = ( formats, country ) => { | |
if( !formats ){ | |
console.log('NO format for', country); | |
return; | |
} | |
const formatters = formats.map( format => { | |
return { | |
// The last leading_digits_pattern is used here, as it is the most detailed | |
leadingDigitsPattern: format.leading_digits_patterns[format.leading_digits_patterns.length - 1], | |
formatter: generateFormatArray( format ), | |
// nationalPrefixFormattingRule: format.national_prefix_formatting_rule | |
} | |
}); | |
if( country === 'US' ){ | |
console.log('WTF', formatters); | |
} | |
return formatters; | |
} | |
/** ------------------------------------------------------------ | |
* What countries to include in output | |
*/ | |
const supported = [ | |
"US", | |
"CA", | |
"PR", | |
"MX", | |
"AE", | |
"JO", | |
"IL", | |
"BE", | |
"HR", | |
"CZ", | |
"DK", | |
"DE", | |
"GR", | |
"ES", | |
"FR", | |
"GB", | |
"IE", | |
"IS", | |
"IT", | |
"LU", | |
"NL", | |
"NO", | |
"AT", | |
"PL", | |
"PT", | |
"PL", | |
"CH", | |
"SE", | |
"FI", | |
"CN", | |
"HK", | |
"MO", | |
"TW", | |
"JP", | |
"KR", | |
"AU", | |
"NZ", | |
"SG", | |
"IN", | |
"ZA", | |
"RO", | |
"HU", | |
"EE", | |
"SI", | |
"SK", | |
"TR", | |
"LT", | |
"LV" | |
] | |
/** ------------------------------------------------------------ | |
* Full output | |
*/ | |
Object.entries(parsedInput.countries).map(([key, value]) => { | |
return { | |
country: key, | |
countryCode: value.phone_code, | |
iddPrefix: value.default_idd_prefix || value.idd_prefix, | |
nddPrefix: value.national_prefix, | |
formatters: generateFormatter( value.formats, key ), | |
examples: value.examples, | |
pattern: value.national_number_pattern | |
} | |
}).filter(c => supported.find( s => s === c.country) ).forEach( c => output[c.country] = c ); | |
/** ------------------------------------------------------------ | |
* Individual outputs | |
*/ | |
// Object.entries(parsedInput.countries).map(([key, value]) => { | |
// return { | |
// country: key, | |
// countryCode: value.phone_code, | |
// iddPrefix: value.default_idd_prefix || value.idd_prefix, | |
// nddPrefix: value.national_prefix, | |
// formatters: generateFormatter( value.formats, key ), | |
// } | |
// }).filter(c => supported.find( s => s === c.country) ).forEach( c => phoneFormats[c.country] = c ); | |
/** ------------------------------------------------------------ | |
* Safety check to look for missing data | |
*/ | |
const keys = Object.keys(output); | |
supported.forEach( m => { | |
if( !keys.find( s => s === m) ) { | |
console.log('Unable to map', m); | |
} | |
}) | |
Object.entries(output).forEach(([key, val]) =>{ if(!val.formatters) console.log('No formatters for', key)} ) | |
/** ------------------------------------------------------------ | |
* Separate out data into sub objects | |
*/ | |
Object.entries(output).forEach(([key, val]) =>{ | |
examples[key] = { | |
fixedline: val.examples.fixed_line, | |
mobile: val.examples.mobile, | |
tollfree: val.examples.toll_free | |
}; | |
patterns[key] = { | |
national: val.pattern, | |
}; | |
phoneFormats[key] = { | |
country: key, | |
countryCode: val.countryCode, | |
iddPrefix: val.idd_prefix, | |
nddPrefix: val.nddPrefix, | |
formatters: val.formatters, | |
} | |
}) | |
fs.writeFileSync('fullOutput.json', JSON.stringify(output, null, 2) ); | |
fs.writeFileSync('formats.json', JSON.stringify(phoneFormats, null, 2) ); | |
fs.writeFileSync('examples.json', JSON.stringify(examples, null, 2) ); | |
fs.writeFileSync('patterns.json', JSON.stringify(patterns, null, 2) ); | |
// ------------------------------------------------------------------------- | |
// Below code is for testing only | |
const formatObj = JSON.parse( fs.readFileSync('fullOutput.json').toString()) | |
// fs.writeFileSync('output.min.json', JSON.stringify(formatObj)); | |
const formatMatches = (format, leadingDigits) => { | |
// Brackets are required for `^` to be applied to | |
// all or-ed (`|`) parts, not just the first one. | |
// new RegExp("^60|8|9").test(11181111) --> true ( this is NOT what we want ) | |
// new RegExp("^(60|8|9)").test(11181111) --> false ( this is what we want ) | |
const leadingDigitsPattern = format.leadingDigitsPattern; | |
return new RegExp(`^(${leadingDigitsPattern})`).test(leadingDigits) | |
} | |
const buildFormatterFunction = ( formatObj ) => { | |
const formatter = ( value ) => { | |
// console.log('WTF', formatObj[value.country]); | |
// Grab formatters array from the formatters object | |
const formatters = formatObj[value.country].formatters; | |
// Now we need to make a guess as to which formatter to use! | |
// Default to the last formatter | |
const matched = formatters.find( format => formatMatches( format, value.number )) || formatters[formatters.length - 1]; | |
// console.log( 'Number:', value.number, 'Formatter:', matched.formatter ); | |
// OLD | |
// const parsedFormatter = matched.formatter.map( m => m.length === 1 ? m : new RegExp(m) ); | |
// NEW | |
const parsedFormatter = matched.formatter; | |
// console.log('Parsed', parsedFormatter); | |
return parsedFormatter; | |
} | |
return formatter; | |
}; | |
const formatFunc = buildFormatterFunction(formatObj); | |
// 11 111 1111 | |
formatFunc({ country: 'AE', number: '501234567' }); | |
// 1 111 1111 | |
formatFunc({ country: 'AE', number: '22345678' }); | |
/* -------------------------- Formatter ----------------------------- */ | |
const formatterFromString = (formatter) => { | |
return formatter.split('').map(char => { | |
if (char === '#') { | |
return /\d/; | |
} | |
if (char === '*') { | |
return /[\w]/; | |
} | |
return char; | |
}); | |
} | |
const getFormatter = (formatter, value) => { | |
// If mask is a string turn it into an array; | |
if (typeof formatter === 'string') { | |
return formatterFromString(formatter); | |
} | |
// If mask is a function use it to genreate current mask | |
if (typeof formatter === 'function') { | |
const fmtr = formatter(value); | |
if (typeof fmtr === 'string') { | |
return formatterFromString(fmtr); | |
} | |
return fmtr; | |
} | |
if (Array.isArray(formatter)) { | |
return formatter; | |
} | |
// Should never make it here throw | |
throw new Error('Formatter must be string, array, or function'); | |
}; | |
const matchingIndex = (a, b) => { | |
let i = 0; | |
let mi = -1; | |
let matching = true; | |
// a = "+1 " | |
// b = "+12" | |
while (matching && i < a.length) { | |
if (a[i] == b[i]) { | |
mi = i; | |
} else { | |
matching = false; | |
} | |
i = i + 1; | |
} | |
return mi; | |
}; | |
const format = (v, frmtr, pointer) => { | |
// console.log('Formatting', value); | |
let value = v; | |
if( pointer ){ | |
value = v[pointer]; | |
} | |
// Null check | |
if (!value) { | |
return { | |
value, | |
offset: 0 | |
}; | |
} | |
// Generate formatter array | |
const formatter = getFormatter(frmtr, v); | |
// Start to fill in the array | |
// Example: phone formatter | |
// formatter =['+', '1', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/] | |
// value examples: | |
// "1231231234 ----> +1 123-123-1234 | |
// "+" ----> + | |
// "+1" ----> +1 | |
// "+2" ----> +1 2 | |
// "1" ----> +1 1 | |
// "1234" ----> +1 123-4 | |
// "123a" ----> +1 123 | |
// Determine prefix length and suffix start | |
const prefixLength = formatter.findIndex(v => typeof v != 'string'); | |
const suffixStart = | |
formatter.length - | |
[...formatter].reverse().findIndex(v => typeof v != 'string'); | |
// Formatted value | |
let formatted = []; | |
// The characters from the current value | |
const chars = value.split(''); | |
// To track the value index during itteration | |
let vIndex = 0; | |
let start = 0; | |
// If the value matches part of the prefix take it out | |
// Example prefix = "+1 " value = ["+1 123-123-1234", "+12", "+2"] | |
const matchIndex = matchingIndex( | |
formatter.slice(0, prefixLength), | |
chars.slice(0, prefixLength) | |
); | |
// console.log('Matching index', matchIndex); | |
if (matchIndex > -1) { | |
//vIndex = prefixLength; | |
vIndex = matchIndex + 1; | |
formatted = formatted.concat(formatter.slice(0, matchIndex + 1)); | |
start = matchIndex + 1; | |
} | |
// Example prefix = "+1 " value=["1", "1234"] | |
if (matchIndex < 0) { | |
// Start past the prefix | |
formatted = formatted.concat(formatter.slice(0, prefixLength)); | |
start = prefixLength; | |
} | |
// console.log('start', start, formatted); | |
// console.log('PREFIX_LENGTHT', prefixLength); | |
// console.log('SUFIX_START', suffixStart); | |
// console.log('FORMATTER_LENGTH', formatter.length); | |
// To track if we have made it past the prefix | |
let pastPrefix = false; | |
// Fill in the stuff | |
for (let i = start; i < formatter.length; i++) { | |
// Get current formatter location matcher | |
const matcher = formatter[i]; | |
// We get past the prefix if matcher is not a string | |
if (!pastPrefix && typeof matcher != 'string') { | |
pastPrefix = true; | |
} | |
// Chec to see if there is more value to look at | |
if (vIndex != chars.length) { | |
// Get the current value character | |
const curChar = chars[vIndex]; | |
// If type is string normal compare otherwise regex compare | |
const match = | |
typeof matcher === 'string' | |
? matcher === curChar | |
: matcher.test(curChar); | |
// If the current character of the value matches and matcher is a string | |
// "1" === "1" | |
if (match && typeof matcher === 'string') { | |
formatted.push(matcher); | |
//if( pastPrefix ){ | |
vIndex = vIndex + 1; | |
//} | |
} | |
// If the current character does not match and matcher is a stirng | |
// "1" != "+" | |
else if (!match && typeof matcher === 'string') { | |
// Special check for 123a ---> dont want "+1 123-" | |
// Special check for 1234 ---> DO want "+1 123-4" | |
if (vIndex != chars.length) formatted.push(matcher); | |
} | |
// If the current character matches and the matcher is not a string | |
// /\d/.test("2") | |
else if (match && typeof matcher != 'string') { | |
formatted.push(curChar); | |
vIndex = vIndex + 1; | |
} | |
// If the current character does NOT match and the matecer is regex | |
// /\d/.test("a") | |
else if (!match && typeof matcher != 'string') { | |
// Throw out this value | |
vIndex = vIndex + 1; | |
i = i - 1; | |
} | |
} else { | |
// If mattcher is a string and we are at suffix keep going | |
if (typeof matcher === 'string' && i >= suffixStart) { | |
formatted.push(matcher); | |
} else { | |
// Otherwise we want to break out | |
break; | |
} | |
} | |
} | |
return { | |
value: formatted.join(''), | |
offset: value ? formatted.length - value.length : 0 | |
}; | |
}; | |
console.log('AE \n----------------------------------------------------------\n'); | |
// AE - mobile - 11 111 1111 | |
console.log( format({ country: 'AE', number: '501234567' }, formatFunc, 'number') ); | |
console.log( format({ country: 'AE', number: '50123' }, formatFunc, 'number') ); | |
// AE - landline - 1 111 1111 | |
console.log(format({ country: 'AE', number: '22345678' }, formatFunc, 'number')); | |
console.log(format({ country: 'AE', number: '2234' }, formatFunc, 'number')); | |
// AE - toll_free - 111 111111 | |
console.log(format({ country: 'AE', number: '800123456' }, formatFunc, 'number')); | |
console.log(format({ country: 'AE', number: '80012' }, formatFunc, 'number')); | |
console.log('\nFrance ----------------------------------------------------------\n'); | |
// France - mobile - 1 11 11 11 11 | |
console.log(format({ country: 'FR', number: '612345678' }, formatFunc, 'number')); | |
// France - landline - 1 11 11 11 11 | |
console.log(format({ country: 'FR', number: '123456789' }, formatFunc, 'number')); | |
// France - toll_free - 111 11 11 11 | |
console.log(format({ country: 'FR', number: '801234567' }, formatFunc, 'number')); | |
console.log('\nUSA ----------------------------------------------------------\n'); | |
// US - mobile - | |
console.log(format({ country: 'US', number: '2015550123' }, formatFunc, 'number')); | |
// US - landline - broken because of a 101 | |
console.log(format({ country: 'US', number: '1015550123' }, formatFunc, 'number')); | |
// US - landline - broken because of a 1 | |
console.log(format({ country: 'US', number: '1345550123' }, formatFunc, 'number')); | |
// US - toll_free - | |
console.log(format({ country: 'US', number: '8002345678' }, formatFunc, 'number')); | |
// NL | |
console.log('\nNetherlands ----------------------------------------------------------\n'); | |
// NL - mobile | |
console.log(format({ country: 'NL', number: '612345678' }, formatFunc, 'number')); | |
// NL - landline | |
console.log(format({ country: 'NL', number: '101234567' }, formatFunc, 'number')); | |
// NL - toll_free | |
console.log(format({ country: 'NL', number: '8001234' }, formatFunc, 'number')); | |
// NL - three digit area code | |
console.log(format({ country: 'NL', number: '184751555' }, formatFunc, 'number')); | |
// NL - pager | |
console.log(format({ country: 'NL', number: '662345678' }, formatFunc, 'number')); | |
// NL - mobile | |
console.log(format({ country: 'NL', number: '6' }, formatFunc, 'number')); | |
// NL - mobile | |
console.log(format({ country: 'NL', number: '61' }, formatFunc, 'number')); | |
// NL - mobile | |
console.log(format({ country: 'NL', number: '66' }, formatFunc, 'number')); | |
console.log('\nIndia ----------------------------------------------------------\n'); | |
// IN - landline | |
console.log(format({ country: 'IN', number: '7410410123' }, formatFunc, 'number')); | |
// IN - mobile | |
console.log(format({ country: 'IN', number: '8123456789' }, formatFunc, 'number')); | |
// IN - toll_free | |
console.log(format({ country: 'IN', number: '1800123456' }, formatFunc, 'number')); | |
// IN - mobile | |
console.log(format({ country: 'IN', number: '9746896764' }, formatFunc, 'number')); | |
// IN - Google Mumbai - +91-22-6611-7200 | |
console.log(format({ country: 'IN', number: '2266117200' }, formatFunc, 'number')); | |
// IN - Google Gurgaon - +91-124-4512900 | |
console.log(format({ country: 'IN', number: '1244512900' }, formatFunc, 'number')); | |
// IN - Tesla - 80-48149455 | |
console.log(format({ country: 'IN', number: '8048149455' }, formatFunc, 'number')); | |
console.log('\nGreat Britian ----------------------------------------------------------\n'); | |
// GB - fixed line birmingham | |
console.log(format({ country: 'GB', number: '1212345678' }, formatFunc, 'number')); | |
console.log(format({ country: 'GB', number: '2012345678' }, formatFunc, 'number')); | |
// Leighton Buzzard | |
console.log(format({ country: 'GB', number: '1525123456' }, formatFunc, 'number')); | |
// London | |
console.log(format({ country: 'GB', number: '2012345678' }, formatFunc, 'number')); | |
// WTF | |
console.log(format({ country: 'GB', number: '1634380140' }, formatFunc, 'number')); | |
// GB - mobile | |
console.log(format({ country: 'GB', number: '7400123456' }, formatFunc, 'number')); | |
// GB - toll free | |
console.log(format({ country: 'GB', number: '8001234567' }, formatFunc, 'number')); | |
console.log('\nGermany ----------------------------------------------------------\n'); | |
// DE - fixed line | |
console.log(format({ country: 'DE', number: '30123456' }, formatFunc, 'number')); | |
// DE - mobile line | |
console.log(format({ country: 'DE', number: '15123456789' }, formatFunc, 'number')); | |
// DE - toll_free line | |
console.log(format({ country: 'DE', number: '8001234567890' }, formatFunc, 'number')); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment