Bumping manifests a=b2g-bump
[gecko.git] / dom / phonenumberutils / PhoneNumber.jsm
blobe22941a7248194456123b3802d38341ffa576c2a
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
4 // Don't modify this code. Please use:
5 // https://github.com/andreasgal/PhoneNumber.js
7 "use strict";
9 this.EXPORTED_SYMBOLS = ["PhoneNumber"];
11 const Cu = Components.utils;
13 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
14 XPCOMUtils.defineLazyModuleGetter(this, "PHONE_NUMBER_META_DATA",
15                                   "resource://gre/modules/PhoneNumberMetaData.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberNormalizer",
17                                   "resource://gre/modules/PhoneNumberNormalizer.jsm");
18 this.PhoneNumber = (function (dataBase) {
19   // Use strict in our context only - users might not want it
20   'use strict';
22   const MAX_PHONE_NUMBER_LENGTH = 50;
23   const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
24   const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
25   const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
26   const BACKSLASH = /\\/g;
27   const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
28   const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
30   // Format of the string encoded meta data. If the name contains "^" or "$"
31   // we will generate a regular expression from the value, with those special
32   // characters as prefix/suffix.
33   const META_DATA_ENCODING = ["region",
34                               "^(?:internationalPrefix)",
35                               "nationalPrefix",
36                               "^(?:nationalPrefixForParsing)",
37                               "nationalPrefixTransformRule",
38                               "nationalPrefixFormattingRule",
39                               "^possiblePattern$",
40                               "^nationalPattern$",
41                               "formats"];
43   const FORMAT_ENCODING = ["^pattern$",
44                            "nationalFormat",
45                            "^leadingDigits",
46                            "nationalPrefixFormattingRule",
47                            "internationalFormat"];
49   var regionCache = Object.create(null);
51   // Parse an array of strings into a convenient object. We store meta
52   // data as arrays since thats much more compact than JSON.
53   function ParseArray(array, encoding, obj) {
54     for (var n = 0; n < encoding.length; ++n) {
55       var value = array[n];
56       if (!value)
57         continue;
58       var field = encoding[n];
59       var fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
60       if (field != fieldAlpha)
61         value = new RegExp(field.replace(fieldAlpha, value));
62       obj[fieldAlpha] = value;
63     }
64     return obj;
65   }
67   // Parse string encoded meta data into a convenient object
68   // representation.
69   function ParseMetaData(countryCode, md) {
70     var array = eval(md.replace(BACKSLASH, "\\\\"));
71     md = ParseArray(array,
72                     META_DATA_ENCODING,
73                     { countryCode: countryCode });
74     regionCache[md.region] = md;
75     return md;
76   }
78   // Parse string encoded format data into a convenient object
79   // representation.
80   function ParseFormat(md) {
81     var formats = md.formats;
82     if (!formats) {
83       return null;
84     }
85     // Bail if we already parsed the format definitions.
86     if (!(Array.isArray(formats[0])))
87       return;
88     for (var n = 0; n < formats.length; ++n) {
89       formats[n] = ParseArray(formats[n],
90                               FORMAT_ENCODING,
91                               {});
92     }
93   }
95   // Search for the meta data associated with a region identifier ("US") in
96   // our database, which is indexed by country code ("1"). Since we have
97   // to walk the entire database for this, we cache the result of the lookup
98   // for future reference.
99   function FindMetaDataForRegion(region) {
100     // Check in the region cache first. This will find all entries we have
101     // already resolved (parsed from a string encoding).
102     var md = regionCache[region];
103     if (md)
104       return md;
105     for (var countryCode in dataBase) {
106       var entry = dataBase[countryCode];
107       // Each entry is a string encoded object of the form '["US..', or
108       // an array of strings. We don't want to parse the string here
109       // to save memory, so we just substring the region identifier
110       // and compare it. For arrays, we compare against all region
111       // identifiers with that country code. We skip entries that are
112       // of type object, because they were already resolved (parsed into
113       // an object), and their country code should have been in the cache.
114       if (Array.isArray(entry)) {
115         for (var n = 0; n < entry.length; n++) {
116           if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) {
117             if (n > 0) {
118               // Only the first entry has the formats field set.
119               // Parse the main country if we haven't already and use
120               // the formats field from the main country.
121               if (typeof entry[0] == "string")
122                 entry[0] = ParseMetaData(countryCode, entry[0]);
123               let formats = entry[0].formats;
124               let current = ParseMetaData(countryCode, entry[n]);
125               current.formats = formats;
126               return entry[n] = current;
127             }
129             entry[n] = ParseMetaData(countryCode, entry[n]);
130             return entry[n];
131           }
132         }
133         continue;
134       }
135       if (typeof entry == "string" && entry.substr(2,2) == region)
136         return dataBase[countryCode] = ParseMetaData(countryCode, entry);
137     }
138   }
140   // Format a national number for a given region. The boolean flag "intl"
141   // indicates whether we want the national or international format.
142   function FormatNumber(regionMetaData, number, intl) {
143     // We lazily parse the format description in the meta data for the region,
144     // so make sure to parse it now if we haven't already done so.
145     ParseFormat(regionMetaData);
146     var formats = regionMetaData.formats;
147     if (!formats) {
148       return null;
149     }
150     for (var n = 0; n < formats.length; ++n) {
151       var format = formats[n];
152       // The leading digits field is optional. If we don't have it, just
153       // use the matching pattern to qualify numbers.
154       if (format.leadingDigits && !format.leadingDigits.test(number))
155         continue;
156       if (!format.pattern.test(number))
157         continue;
158       if (intl) {
159         // If there is no international format, just fall back to the national
160         // format.
161         var internationalFormat = format.internationalFormat;
162         if (!internationalFormat)
163           internationalFormat = format.nationalFormat;
164         // Some regions have numbers that can't be dialed from outside the
165         // country, indicated by "NA" for the international format of that
166         // number format pattern.
167         if (internationalFormat == "NA")
168           return null;
169         // Prepend "+" and the country code.
170         number = "+" + regionMetaData.countryCode + " " +
171                  number.replace(format.pattern, internationalFormat);
172       } else {
173         number = number.replace(format.pattern, format.nationalFormat);
174         // The region has a national prefix formatting rule, and it can be overwritten
175         // by each actual number format rule.
176         var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
177         if (format.nationalPrefixFormattingRule)
178           nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
179         if (nationalPrefixFormattingRule) {
180           // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
181           // "$NP" will be replaced by the national prefix, and "$FG" with the
182           // first group of numbers.
183           var match = number.match(SPLIT_FIRST_GROUP);
184           if (match) {
185             var firstGroup = match[1];
186             var rest = match[2];
187             var prefix = nationalPrefixFormattingRule;
188             prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
189             prefix = prefix.replace("$FG", firstGroup);
190             number = prefix + rest;
191           }
192         }
193       }
194       return (number == "NA") ? null : number;
195     }
196     return null;
197   }
199   function NationalNumber(regionMetaData, number) {
200     this.region = regionMetaData.region;
201     this.regionMetaData = regionMetaData;
202     this.nationalNumber = number;
203   }
205   // NationalNumber represents the result of parsing a phone number. We have
206   // three getters on the prototype that format the number in national and
207   // international format. Once called, the getters put a direct property
208   // onto the object, caching the result.
209   NationalNumber.prototype = {
210     // +1 949-726-2896
211     get internationalFormat() {
212       var value = FormatNumber(this.regionMetaData, this.nationalNumber, true);
213       Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true });
214       return value;
215     },
216     // (949) 726-2896
217     get nationalFormat() {
218       var value = FormatNumber(this.regionMetaData, this.nationalNumber, false);
219       Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true });
220       return value;
221     },
222     // +19497262896
223     get internationalNumber() {
224       var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
225                                            : null;
226       Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true });
227       return value;
228     },
229     // country name 'US'
230     get countryName() {
231       var value = this.region ? this.region : null;
232       Object.defineProperty(this, "countryName", { value: value, enumerable: true });
233       return value;
234     }
235   };
237   // Check whether the number is valid for the given region.
238   function IsValidNumber(number, md) {
239     return md.possiblePattern.test(number);
240   }
242   // Check whether the number is a valid national number for the given region.
243   function IsNationalNumber(number, md) {
244     return IsValidNumber(number, md) && md.nationalPattern.test(number);
245   }
247   // Determine the country code a number starts with, or return null if
248   // its not a valid country code.
249   function ParseCountryCode(number) {
250     for (var n = 1; n <= 3; ++n) {
251       var cc = number.substr(0,n);
252       if (dataBase[cc])
253         return cc;
254     }
255     return null;
256   }
258   // Parse an international number that starts with the country code. Return
259   // null if the number is not a valid international number.
260   function ParseInternationalNumber(number) {
261     var ret;
263     // Parse and strip the country code.
264     var countryCode = ParseCountryCode(number);
265     if (!countryCode)
266       return null;
267     number = number.substr(countryCode.length);
269     // Lookup the meta data for the region (or regions) and if the rest of
270     // the number parses for that region, return the parsed number.
271     var entry = dataBase[countryCode];
272     if (Array.isArray(entry)) {
273       for (var n = 0; n < entry.length; ++n) {
274         if (typeof entry[n] == "string")
275           entry[n] = ParseMetaData(countryCode, entry[n]);
276         if (n > 0)
277           entry[n].formats = entry[0].formats;
278         ret = ParseNationalNumber(number, entry[n])
279         if (ret)
280           return ret;
281       }
282       return null;
283     }
284     if (typeof entry == "string")
285       entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
286     return ParseNationalNumber(number, entry);
287   }
289   // Parse a national number for a specific region. Return null if the
290   // number is not a valid national number (it might still be a possible
291   // number for parts of that region).
292   function ParseNationalNumber(number, md) {
293     if (!md.possiblePattern.test(number) ||
294         !md.nationalPattern.test(number)) {
295       return null;
296     }
297     // Success.
298     return new NationalNumber(md, number);
299   }
301   // Parse a number and transform it into the national format, removing any
302   // international dial prefixes and country codes.
303   function ParseNumber(number, defaultRegion) {
304     var ret;
306     // Remove formating characters and whitespace.
307     number = PhoneNumberNormalizer.Normalize(number);
309     // If there is no defaultRegion or the defaultRegion is the global region,
310     // we can't parse international access codes.
311     if ((!defaultRegion || defaultRegion === '001') && number[0] !== '+')
312       return null;
314     // Detect and strip leading '+'.
315     if (number[0] === '+')
316       return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, ""));
318     // Lookup the meta data for the given region.
319     var md = FindMetaDataForRegion(defaultRegion.toUpperCase());
321     if (!md) {
322       dump("Couldn't find Meta Data for region: " + defaultRegion + "\n");
323       return null;
324     }
326     // See if the number starts with an international prefix, and if the
327     // number resulting from stripping the code is valid, then remove the
328     // prefix and flag the number as international.
329     if (md.internationalPrefix.test(number)) {
330       var possibleNumber = number.replace(md.internationalPrefix, "");
331       ret = ParseInternationalNumber(possibleNumber)
332       if (ret)
333         return ret;
334     }
336     // This is not an international number. See if its a national one for
337     // the current region. National numbers can start with the national
338     // prefix, or without.
339     if (md.nationalPrefixForParsing) {
340       // Some regions have specific national prefix parse rules. Apply those.
341       var withoutPrefix = number.replace(md.nationalPrefixForParsing,
342                                          md.nationalPrefixTransformRule || '');
343       ret = ParseNationalNumber(withoutPrefix, md)
344       if (ret)
345         return ret;
346     } else {
347       // If there is no specific national prefix rule, just strip off the
348       // national prefix from the beginning of the number (if there is one).
349       var nationalPrefix = md.nationalPrefix;
350       if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
351           (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
352         return ret;
353       }
354     }
355     ret = ParseNationalNumber(number, md)
356     if (ret)
357       return ret;
359     // Now lets see if maybe its an international number after all, but
360     // without '+' or the international prefix.
361     ret = ParseInternationalNumber(number)
362     if (ret)
363       return ret;
365     // If the number matches the possible numbers of the current region,
366     // return it as a possible number.
367     if (md.possiblePattern.test(number))
368       return new NationalNumber(md, number);
370     // We couldn't parse the number at all.
371     return null;
372   }
374   function IsPlainPhoneNumber(number) {
375     if (typeof number !== 'string') {
376       return false;
377     }
379     var length = number.length;
380     var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH);
381     var isEmpty = (length === 0);
382     return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
383   }
385   return {
386     IsPlain: IsPlainPhoneNumber,
387     Parse: ParseNumber,
388   };
389 })(PHONE_NUMBER_META_DATA);