Skip to content

Instantly share code, notes, and snippets.

@joepuzzo
Last active February 22, 2022 16:16
Show Gist options
  • Save joepuzzo/677f68b29e18ac7500316f2a6a8497e8 to your computer and use it in GitHub Desktop.
Save joepuzzo/677f68b29e18ac7500316f2a6a8497e8 to your computer and use it in GitHub Desktop.
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