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;