1 /*
  2  * handler.js - Handle phone number parse states
  3  *
  4  * Copyright © 2014-2015, JEDLSoft
  5  *
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  *
 10  *     http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *
 16  * See the License for the specific language governing permissions and
 17  * limitations under the License.
 18  */
 19 
 20 
 21 /**
 22  * @class
 23  * @private
 24  * @constructor
 25  */
 26 var PhoneHandler = function () {
 27     return this;
 28 };
 29 
 30 PhoneHandler.prototype = {
 31     /**
 32      * @private
 33      * @param {string} number phone number
 34      * @param {Object} fields the fields that have been extracted so far
 35      * @param {Object} regionSettings settings used to parse the rest of the number
 36      */
 37     processSubscriberNumber: function(number, fields, regionSettings) {
 38         var last;
 39 
 40         last = number.search(/[xwtp,;]/i);    // last digit of the local number
 41 
 42         if (last > -1) {
 43             if (last > 0) {
 44                 fields.subscriberNumber = number.substring(0, last);
 45             }
 46             // strip x's which are there to indicate a break between the local subscriber number and the extension, but
 47             // are not themselves a dialable character
 48             fields.extension = number.substring(last).replace('x', '');
 49         } else {
 50             if (number.length) {
 51                 fields.subscriberNumber = number;
 52             }
 53         }
 54 
 55         if (regionSettings.plan.getFieldLength('maxLocalLength') &&
 56                 fields.subscriberNumber &&
 57                 fields.subscriberNumber.length > regionSettings.plan.getFieldLength('maxLocalLength')) {
 58             fields.invalid = true;
 59         }
 60     },
 61     /**
 62      * @private
 63      * @param {string} fieldName
 64      * @param {number} length length of phone number
 65      * @param {string} number phone number
 66      * @param {number} currentChar currentChar to be parsed
 67      * @param {Object} fields the fields that have been extracted so far
 68      * @param {Object} regionSettings settings used to parse the rest of the number
 69      * @param {boolean} noExtractTrunk
 70      */
 71     processFieldWithSubscriberNumber: function(fieldName, length, number, currentChar, fields, regionSettings, noExtractTrunk) {
 72         var ret, end;
 73 
 74         if (length !== undefined && length > 0) {
 75             // fixed length
 76             end = length;
 77             if (regionSettings.plan.getTrunkCode() === "0" && number.charAt(0) === "0") {
 78                 end += regionSettings.plan.getTrunkCode().length;  // also extract the trunk access code
 79             }
 80         } else {
 81             // variable length
 82             // the setting is the negative of the length to add, so subtract to make it positive
 83             end = currentChar + 1 - length;
 84         }
 85 
 86         if (fields[fieldName] !== undefined) {
 87             // we have a spurious recognition, because this number already contains that field! So, just put
 88             // everything into the subscriberNumber as the default
 89             this.processSubscriberNumber(number, fields, regionSettings);
 90         } else {
 91             fields[fieldName] = number.substring(0, end);
 92             if (number.length > end) {
 93                 this.processSubscriberNumber(number.substring(end), fields, regionSettings);
 94             }
 95         }
 96 
 97         ret = {
 98             number: ""
 99         };
100 
101         return ret;
102     },
103     /**
104      * @private
105      * @param {string} fieldName
106      * @param {number} length length of phone number
107      * @param {string} number phone number
108      * @param {number} currentChar currentChar to be parsed
109      * @param {Object} fields the fields that have been extracted so far
110      * @param {Object} regionSettings settings used to parse the rest of the number
111      */
112     processField: function(fieldName, length, number, currentChar, fields, regionSettings) {
113         var ret = {}, end;
114 
115         if (length !== undefined && length > 0) {
116             // fixed length
117             end = length;
118             if (regionSettings.plan.getTrunkCode() === "0" && number.charAt(0) === "0") {
119                 end += regionSettings.plan.getTrunkCode().length;  // also extract the trunk access code
120             }
121         } else {
122             // variable length
123             // the setting is the negative of the length to add, so subtract to make it positive
124             end = currentChar + 1 - length;
125         }
126 
127         if (fields[fieldName] !== undefined) {
128             // we have a spurious recognition, because this number already contains that field! So, just put
129             // everything into the subscriberNumber as the default
130             this.processSubscriberNumber(number, fields, regionSettings);
131             ret.number = "";
132         } else {
133             fields[fieldName] = number.substring(0, end);
134             ret.number = (number.length > end) ? number.substring(end) : "";
135         }
136 
137         return ret;
138     },
139     /**
140      * @private
141      * @param {string} number phone number
142      * @param {number} currentChar currentChar to be parsed
143      * @param {Object} fields the fields that have been extracted so far
144      * @param {Object} regionSettings settings used to parse the rest of the number
145      */
146     trunk: function(number, currentChar, fields, regionSettings) {
147         var ret, trunkLength;
148 
149         if (fields.trunkAccess !== undefined) {
150             // What? We already have one? Okay, put the rest of this in the subscriber number as the default behaviour then.
151             this.processSubscriberNumber(number, fields, regionSettings);
152             number = "";
153         } else {
154             trunkLength = regionSettings.plan.getTrunkCode().length;
155             fields.trunkAccess = number.substring(0, trunkLength);
156             number = (number.length > trunkLength) ? number.substring(trunkLength) : "";
157         }
158 
159         ret = {
160             number: number
161         };
162 
163         return ret;
164     },
165     /**
166      * @private
167      * @param {string} number phone number
168      * @param {number} currentChar currentChar to be parsed
169      * @param {Object} fields the fields that have been extracted so far
170      * @param {Object} regionSettings settings used to parse the rest of the number
171      */
172     plus: function(number, currentChar, fields, regionSettings) {
173         var ret = {};
174 
175         if (fields.iddPrefix !== undefined) {
176             // What? We already have one? Okay, put the rest of this in the subscriber number as the default behaviour then.
177             this.processSubscriberNumber(number, fields, regionSettings);
178             ret.number = "";
179         } else {
180             // found the idd prefix, so save it and cause the function to parse the next part
181             // of the number with the idd table
182             fields.iddPrefix = number.substring(0, 1);
183 
184             ret = {
185                 number: number.substring(1),
186                 table: 'idd'    // shared subtable that parses the country code
187             };
188         }
189         return ret;
190     },
191     /**
192      * @private
193      * @param {string} number phone number
194      * @param {number} currentChar currentChar to be parsed
195      * @param {Object} fields the fields that have been extracted so far
196      * @param {Object} regionSettings settings used to parse the rest of the number
197      */
198     idd: function(number, currentChar, fields, regionSettings) {
199         var ret = {};
200 
201         if (fields.iddPrefix !== undefined) {
202             // What? We already have one? Okay, put the rest of this in the subscriber number as the default behaviour then.
203             this.processSubscriberNumber(number, fields, regionSettings);
204             ret.number = "";
205         } else {
206             // found the idd prefix, so save it and cause the function to parse the next part
207             // of the number with the idd table
208             fields.iddPrefix = number.substring(0, currentChar+1);
209 
210             ret = {
211                 number: number.substring(currentChar+1),
212                 table: 'idd'    // shared subtable that parses the country code
213             };
214         }
215 
216         return ret;
217     },
218     /**
219      * @private
220      * @param {string} number phone number
221      * @param {number} currentChar currentChar to be parsed
222      * @param {Object} fields the fields that have been extracted so far
223      * @param {Object} regionSettings settings used to parse the rest of the number
224      */
225     country: function(number, currentChar, fields, regionSettings) {
226         var ret, cc;
227 
228         // found the country code of an IDD number, so save it and cause the function to
229         // parse the rest of the number with the regular table for this locale
230         fields.countryCode = number.substring(0, currentChar+1);
231         cc = fields.countryCode.replace(/[wWpPtT\+#\*]/g, ''); // fix for NOV-108200
232         // console.log("Found country code " + fields.countryCode + ". Switching to country " + locale.region + " to parse the rest of the number");
233 
234         ret = {
235             number: number.substring(currentChar+1),
236             countryCode: cc
237         };
238 
239         return ret;
240     },
241     /**
242      * @private
243      * @param {string} number phone number
244      * @param {number} currentChar currentChar to be parsed
245      * @param {Object} fields the fields that have been extracted so far
246      * @param {Object} regionSettings settings used to parse the rest of the number
247      */
248     cic: function(number, currentChar, fields, regionSettings) {
249         return this.processField('cic', regionSettings.plan.getFieldLength('cic'), number, currentChar, fields, regionSettings);
250     },
251     /**
252      * @private
253      * @param {string} number phone number
254      * @param {number} currentChar currentChar to be parsed
255      * @param {Object} fields the fields that have been extracted so far
256      * @param {Object} regionSettings settings used to parse the rest of the number
257      */
258     service: function(number, currentChar, fields, regionSettings) {
259         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('serviceCode'), number, currentChar, fields, regionSettings, false);
260     },
261     /**
262      * @private
263      * @param {string} number phone number
264      * @param {number} currentChar currentChar to be parsed
265      * @param {Object} fields the fields that have been extracted so far
266      * @param {Object} regionSettings settings used to parse the rest of the number
267      */
268     area: function(number, currentChar, fields, regionSettings) {
269         var ret, last, end, localLength;
270 
271         last = number.search(/[xwtp]/i);    // last digit of the local number
272         localLength = (last > -1) ? last : number.length;
273 
274         if (regionSettings.plan.getFieldLength('areaCode') > 0) {
275             // fixed length
276             end = regionSettings.plan.getFieldLength('areaCode');
277             if (regionSettings.plan.getTrunkCode() === number.charAt(0)) {
278                 end += regionSettings.plan.getTrunkCode().length;  // also extract the trunk access code
279                 localLength -= regionSettings.plan.getTrunkCode().length;
280             }
281         } else {
282             // variable length
283             // the setting is the negative of the length to add, so subtract to make it positive
284             end = currentChar + 1 - regionSettings.plan.getFieldLength('areaCode');
285         }
286 
287         // substring() extracts the part of the string up to but not including the end character,
288         // so add one to compensate
289         if (regionSettings.plan.getFieldLength('maxLocalLength') !== undefined) {
290             if (fields.trunkAccess !== undefined || fields.mobilePrefix !== undefined ||
291                     fields.countryCode !== undefined ||
292                     localLength > regionSettings.plan.getFieldLength('maxLocalLength')) {
293                 // too long for a local number by itself, or a different final state already parsed out the trunk
294                 // or mobile prefix, then consider the rest of this number to be an area code + part of the subscriber number
295                 fields.areaCode = number.substring(0, end);
296                 if (number.length > end) {
297                     this.processSubscriberNumber(number.substring(end), fields, regionSettings);
298                 }
299             } else {
300                 // shorter than the length needed for a local number, so just consider it a local number
301                 this.processSubscriberNumber(number, fields, regionSettings);
302             }
303         } else {
304             fields.areaCode = number.substring(0, end);
305             if (number.length > end) {
306                 this.processSubscriberNumber(number.substring(end), fields, regionSettings);
307             }
308         }
309 
310         // extensions are separated from the number by a dash in Germany
311         if (regionSettings.plan.getFindExtensions() !== undefined && fields.subscriberNumber !== undefined) {
312             var dash = fields.subscriberNumber.indexOf("-");
313             if (dash > -1) {
314                 fields.subscriberNumber = fields.subscriberNumber.substring(0, dash);
315                 fields.extension = fields.subscriberNumber.substring(dash+1);
316             }
317         }
318 
319         ret = {
320             number: ""
321         };
322 
323         return ret;
324     },
325     /**
326      * @private
327      * @param {string} number phone number
328      * @param {number} currentChar currentChar to be parsed
329      * @param {Object} fields the fields that have been extracted so far
330      * @param {Object} regionSettings settings used to parse the rest of the number
331      */
332     none: function(number, currentChar, fields, regionSettings) {
333         var ret;
334 
335         // this is a last resort function that is called when nothing is recognized.
336         // When this happens, just put the whole stripped number into the subscriber number
337 
338         if (number.length > 0) {
339             this.processSubscriberNumber(number, fields, regionSettings);
340             if (currentChar > 0 && currentChar < number.length) {
341                 // if we were part-way through parsing, and we hit an invalid digit,
342                 // indicate that the number could not be parsed properly
343                 fields.invalid = true;
344             }
345         }
346 
347         ret = {
348             number:""
349         };
350 
351         return ret;
352     },
353     /**
354      * @private
355      * @param {string} number phone number
356      * @param {number} currentChar currentChar to be parsed
357      * @param {Object} fields the fields that have been extracted so far
358      * @param {Object} regionSettings settings used to parse the rest of the number
359      */
360     vsc: function(number, currentChar, fields, regionSettings) {
361         var ret, length, end;
362 
363         if (fields.vsc === undefined) {
364             length = regionSettings.plan.getFieldLength('vsc') || 0;
365             if (length !== undefined && length > 0) {
366                 // fixed length
367                 end = length;
368             } else {
369                 // variable length
370                 // the setting is the negative of the length to add, so subtract to make it positive
371                 end = currentChar + 1 - length;
372             }
373 
374             // found a VSC code (ie. a "star code"), so save it and cause the function to
375             // parse the rest of the number with the same table for this locale
376             fields.vsc = number.substring(0, end);
377             number = (number.length > end) ? "^" + number.substring(end) : "";
378         } else {
379             // got it twice??? Okay, this is a bogus number then. Just put everything else into the subscriber number as the default
380             this.processSubscriberNumber(number, fields, regionSettings);
381             number = "";
382         }
383 
384         // treat the rest of the number as if it were a completely new number
385         ret = {
386             number: number
387         };
388 
389         return ret;
390     },
391     /**
392      * @private
393      * @param {string} number phone number
394      * @param {number} currentChar currentChar to be parsed
395      * @param {Object} fields the fields that have been extracted so far
396      * @param {Object} regionSettings settings used to parse the rest of the number
397      */
398     cell: function(number, currentChar, fields, regionSettings) {
399         return this.processFieldWithSubscriberNumber('mobilePrefix', regionSettings.plan.getFieldLength('mobilePrefix'), number, currentChar, fields, regionSettings, false);
400     },
401     /**
402      * @private
403      * @param {string} number phone number
404      * @param {number} currentChar currentChar to be parsed
405      * @param {Object} fields the fields that have been extracted so far
406      * @param {Object} regionSettings settings used to parse the rest of the number
407      */
408     personal: function(number, currentChar, fields, regionSettings) {
409         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('personal'), number, currentChar, fields, regionSettings, false);
410     },
411     /**
412      * @private
413      * @param {string} number phone number
414      * @param {number} currentChar currentChar to be parsed
415      * @param {Object} fields the fields that have been extracted so far
416      * @param {Object} regionSettings settings used to parse the rest of the number
417      */
418     emergency: function(number, currentChar, fields, regionSettings) {
419         return this.processFieldWithSubscriberNumber('emergency', regionSettings.plan.getFieldLength('emergency'), number, currentChar, fields, regionSettings, true);
420     },
421     /**
422      * @private
423      * @param {string} number phone number
424      * @param {number} currentChar currentChar to be parsed
425      * @param {Object} fields the fields that have been extracted so far
426      * @param {Object} regionSettings settings used to parse the rest of the number
427      */
428     premium: function(number, currentChar, fields, regionSettings) {
429         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('premium'), number, currentChar, fields, regionSettings, false);
430     },
431     /**
432      * @private
433      * @param {string} number phone number
434      * @param {number} currentChar currentChar to be parsed
435      * @param {Object} fields the fields that have been extracted so far
436      * @param {Object} regionSettings settings used to parse the rest of the number
437      */
438     special: function(number, currentChar, fields, regionSettings) {
439         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('special'), number, currentChar, fields, regionSettings, false);
440     },
441     /**
442      * @private
443      * @param {string} number phone number
444      * @param {number} currentChar currentChar to be parsed
445      * @param {Object} fields the fields that have been extracted so far
446      * @param {Object} regionSettings settings used to parse the rest of the number
447      */
448     service2: function(number, currentChar, fields, regionSettings) {
449         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('service2'), number, currentChar, fields, regionSettings, false);
450     },
451     /**
452      * @private
453      * @param {string} number phone number
454      * @param {number} currentChar currentChar to be parsed
455      * @param {Object} fields the fields that have been extracted so far
456      * @param {Object} regionSettings settings used to parse the rest of the number
457      */
458     service3: function(number, currentChar, fields, regionSettings) {
459         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('service3'), number, currentChar, fields, regionSettings, false);
460     },
461     /**
462      * @private
463      * @param {string} number phone number
464      * @param {number} currentChar currentChar to be parsed
465      * @param {Object} fields the fields that have been extracted so far
466      * @param {Object} regionSettings settings used to parse the rest of the number
467      */
468     service4: function(number, currentChar, fields, regionSettings) {
469         return this.processFieldWithSubscriberNumber('serviceCode', regionSettings.plan.getFieldLength('service4'), number, currentChar, fields, regionSettings, false);
470     },
471     /**
472      * @private
473      * @param {string} number phone number
474      * @param {number} currentChar currentChar to be parsed
475      * @param {Object} fields the fields that have been extracted so far
476      * @param {Object} regionSettings settings used to parse the rest of the number
477      */
478     cic2: function(number, currentChar, fields, regionSettings) {
479         return this.processField('cic', regionSettings.plan.getFieldLength('cic2'), number, currentChar, fields, regionSettings);
480     },
481     /**
482      * @private
483      * @param {string} number phone number
484      * @param {number} currentChar currentChar to be parsed
485      * @param {Object} fields the fields that have been extracted so far
486      * @param {Object} regionSettings settings used to parse the rest of the number
487      */
488     cic3: function(number, currentChar, fields, regionSettings) {
489         return this.processField('cic', regionSettings.plan.getFieldLength('cic3'), number, currentChar, fields, regionSettings);
490     },
491     /**
492      * @private
493      * @param {string} number phone number
494      * @param {number} currentChar currentChar to be parsed
495      * @param {Object} fields the fields that have been extracted so far
496      * @param {Object} regionSettings settings used to parse the rest of the number
497      */
498     start: function(number, currentChar, fields, regionSettings) {
499         // don't do anything except transition to the next state
500         return {
501             number: number
502         };
503     },
504     /**
505      * @private
506      * @param {string} number phone number
507      * @param {number} currentChar currentChar to be parsed
508      * @param {Object} fields the fields that have been extracted so far
509      * @param {Object} regionSettings settings used to parse the rest of the number
510      */
511     local: function(number, currentChar, fields, regionSettings) {
512         // in open dialling plans, we can tell that this number is a local subscriber number because it
513         // starts with a digit that indicates as such
514         this.processSubscriberNumber(number, fields, regionSettings);
515         return {
516             number: ""
517         };
518     }
519 };
520 
521 // context-sensitive handler
522 /**
523  * @class
524  * @private
525  * @extends PhoneHandler
526  * @constructor
527  */
528 var CSStateHandler = function () {
529     return this;
530 };
531 
532 CSStateHandler.prototype = new PhoneHandler();
533 CSStateHandler.prototype.special = function (number, currentChar, fields, regionSettings) {
534     var ret;
535 
536     // found a special area code that is both a node and a leaf. In
537     // this state, we have found the leaf, so chop off the end
538     // character to make it a leaf.
539     if (number.charAt(0) === "0") {
540         fields.trunkAccess = number.charAt(0);
541         fields.areaCode = number.substring(1, currentChar);
542     } else {
543         fields.areaCode = number.substring(0, currentChar);
544     }
545     this.processSubscriberNumber(number.substring(currentChar), fields, regionSettings);
546 
547     ret = {
548         number: ""
549     };
550 
551     return ret;
552 };
553 
554 /**
555  * @class
556  * @private
557  * @extends PhoneHandler
558  * @constructor
559  */
560 var USStateHandler = function () {
561     return this;
562 };
563 
564 USStateHandler.prototype = new PhoneHandler();
565 USStateHandler.prototype.vsc = function (number, currentChar, fields, regionSettings) {
566     var ret;
567 
568     // found a VSC code (ie. a "star code")
569     fields.vsc = number;
570 
571     // treat the rest of the number as if it were a completely new number
572     ret = {
573         number: ""
574     };
575 
576     return ret;
577 };
578 
579 /**
580  * Creates a phone handler instance that is correct for the locale. Phone handlers are
581  * used to handle parsing of the various fields in a phone number.
582  *
583  * @private
584  * @static
585  * @return {PhoneHandler} the correct phone handler for the locale
586  */
587 var PhoneHandlerFactory = function (locale, plan) {
588     if (plan.getContextFree() !== undefined && typeof(plan.getContextFree()) === 'boolean' && plan.getContextFree() === false) {
589         return new CSStateHandler();
590     }
591     var region = (locale && locale.getRegion()) || "ZZ";
592     switch (region) {
593     case 'US':
594         return new USStateHandler();
595 
596     default:
597         return new PhoneHandler();
598     }
599 };
600 
601 module.exports = PhoneHandlerFactory;