1 /* 2 * IString.js - ilib string subclass definition 3 * 4 * Copyright © 2012-2015, 2018, 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 // !data plurals 21 22 var ilib = require("./ilib.js"); 23 var Utils = require("./Utils.js"); 24 var MathUtils = require("./MathUtils.js"); 25 var Locale = require("./Locale.js"); 26 27 /** 28 * @class 29 * Create a new ilib string instance. This string inherits from and 30 * extends the Javascript String class. It can be 31 * used almost anywhere that a normal Javascript string is used, though in 32 * some instances you will need to call the {@link #toString} method when 33 * a built-in Javascript string is needed. The formatting methods are 34 * methods that are not in the intrinsic String class and are most useful 35 * when localizing strings in an app or web site in combination with 36 * the ResBundle class.<p> 37 * 38 * This class is named IString ("ilib string") so as not to conflict with the 39 * built-in Javascript String class. 40 * 41 * @constructor 42 * @param {string|IString=} string initialize this instance with this string 43 */ 44 var IString = function (string) { 45 if (typeof(string) === 'object') { 46 if (string instanceof IString) { 47 this.str = string.str; 48 } else { 49 this.str = string.toString(); 50 } 51 } else if (typeof(string) === 'string') { 52 this.str = new String(string); 53 } else { 54 this.str = ""; 55 } 56 this.length = this.str.length; 57 this.cpLength = -1; 58 this.localeSpec = ilib.getLocale(); 59 }; 60 61 /** 62 * Return true if the given character is a Unicode surrogate character, 63 * either high or low. 64 * 65 * @private 66 * @static 67 * @param {string} ch character to check 68 * @return {boolean} true if the character is a surrogate 69 */ 70 IString._isSurrogate = function (ch) { 71 var n = ch.charCodeAt(0); 72 return ((n >= 0xDC00 && n <= 0xDFFF) || (n >= 0xD800 && n <= 0xDBFF)); 73 }; 74 75 // build in the English rule 76 IString.plurals_default = { 77 "one": { 78 "and": [ 79 { 80 "eq": [ 81 "i", 82 1 83 ] 84 }, 85 { 86 "eq": [ 87 "v", 88 0 89 ] 90 } 91 ] 92 } 93 }; 94 95 /** 96 * Convert a UCS-4 code point to a Javascript string. The codepoint can be any valid 97 * UCS-4 Unicode character, including supplementary characters. Standard Javascript 98 * only supports supplementary characters using the UTF-16 encoding, which has 99 * values in the range 0x0000-0xFFFF. String.fromCharCode() will only 100 * give you a string containing 16-bit characters, and will not properly convert 101 * the code point for a supplementary character (which has a value > 0xFFFF) into 102 * two UTF-16 surrogate characters. Instead, it will just just give you whatever 103 * single character happens to be the same as your code point modulo 0x10000, which 104 * is almost never what you want.<p> 105 * 106 * Similarly, that means if you use String.charCodeAt() 107 * you will only retrieve a 16-bit value, which may possibly be a single 108 * surrogate character that is part of a surrogate pair representing a character 109 * in the supplementary plane. It will not give you a code point. Use 110 * IString.codePointAt() to access code points in a string, or use 111 * an iterator to walk through the code points in a string. 112 * 113 * @static 114 * @param {number} codepoint UCS-4 code point to convert to a character 115 * @return {string} a string containing the character represented by the codepoint 116 */ 117 IString.fromCodePoint = function (codepoint) { 118 if (codepoint < 0x10000) { 119 return String.fromCharCode(codepoint); 120 } else { 121 var high = Math.floor(codepoint / 0x10000) - 1; 122 var low = codepoint & 0xFFFF; 123 124 return String.fromCharCode(0xD800 | ((high & 0x000F) << 6) | ((low & 0xFC00) >> 10)) + 125 String.fromCharCode(0xDC00 | (low & 0x3FF)); 126 } 127 }; 128 129 /** 130 * Convert the character or the surrogate pair at the given 131 * index into the intrinsic Javascript string to a Unicode 132 * UCS-4 code point. 133 * 134 * @static 135 * @param {string} str string to get the code point from 136 * @param {number} index index into the string 137 * @return {number} code point of the character at the 138 * given index into the string 139 */ 140 IString.toCodePoint = function(str, index) { 141 if (!str || str.length === 0) { 142 return -1; 143 } 144 var code = -1, high = str.charCodeAt(index); 145 if (high >= 0xD800 && high <= 0xDBFF) { 146 if (str.length > index+1) { 147 var low = str.charCodeAt(index+1); 148 if (low >= 0xDC00 && low <= 0xDFFF) { 149 code = (((high & 0x3C0) >> 6) + 1) << 16 | 150 (((high & 0x3F) << 10) | (low & 0x3FF)); 151 } 152 } 153 } else { 154 code = high; 155 } 156 157 return code; 158 }; 159 160 /** 161 * Load the plural the definitions of plurals for the locale. 162 * @param {boolean=} sync 163 * @param {Locale|string=} locale 164 * @param {Object=} loadParams 165 * @param {function(*)=} onLoad 166 */ 167 IString.loadPlurals = function (sync, locale, loadParams, onLoad) { 168 var loc; 169 if (locale) { 170 loc = (typeof(locale) === 'string') ? new Locale(locale) : locale; 171 } else { 172 loc = new Locale(ilib.getLocale()); 173 } 174 var spec = loc.getLanguage(); 175 Utils.loadData({ 176 name: "plurals.json", 177 object: "IString", 178 locale: loc, 179 sync: sync, 180 loadParams: loadParams, 181 callback: ilib.bind(this, function(plurals) { 182 plurals = plurals || IString.plurals_default; 183 if (onLoad && typeof(onLoad) === 'function') { 184 onLoad(plurals); 185 } 186 }) 187 }); 188 }; 189 190 /** 191 * @private 192 * @static 193 */ 194 IString._fncs = { 195 /** 196 * @private 197 * @param {Object} obj 198 * @return {string|undefined} 199 */ 200 firstProp: function (obj) { 201 for (var p in obj) { 202 if (p && obj[p]) { 203 return p; 204 } 205 } 206 return undefined; // should never get here 207 }, 208 209 /** 210 * @private 211 * @param {Object} obj 212 * @return {string|undefined} 213 */ 214 firstPropRule: function (obj) { 215 if (Object.prototype.toString.call(obj) === '[object Array]') { 216 return "inrange"; 217 } else if (Object.prototype.toString.call(obj) === '[object Object]') { 218 for (var p in obj) { 219 if (p && obj[p]) { 220 return p; 221 } 222 } 223 224 } 225 return undefined; // should never get here 226 }, 227 228 /** 229 * @private 230 * @param {Object} obj 231 * @param {number|Object} n 232 * @return {?} 233 */ 234 getValue: function (obj, n) { 235 if (typeof(obj) === 'object') { 236 var subrule = IString._fncs.firstPropRule(obj); 237 if (subrule === "inrange") { 238 return IString._fncs[subrule](obj, n); 239 } 240 return IString._fncs[subrule](obj[subrule], n); 241 } else if (typeof(obj) === 'string') { 242 if (typeof(n) === 'object'){ 243 return n[obj]; 244 } 245 return n; 246 } else { 247 return obj; 248 } 249 }, 250 251 /** 252 * @private 253 * @param {number|Object} n 254 * @param {Array.<number|Array.<number>>|Object} range 255 * @return {boolean} 256 */ 257 matchRangeContinuous: function(n, range) { 258 259 for (var num in range) { 260 if (typeof(num) !== 'undefined' && typeof(range[num]) !== 'undefined') { 261 var obj = range[num]; 262 if (typeof(obj) === 'number') { 263 if (n === range[num]) { 264 return true; 265 } else if (n >= range[0] && n <= range[1]) { 266 return true; 267 } 268 } else if (Object.prototype.toString.call(obj) === '[object Array]') { 269 if (n >= obj[0] && n <= obj[1]) { 270 return true; 271 } 272 } 273 } 274 } 275 return false; 276 }, 277 278 /** 279 * @private 280 * @param {*} number 281 * @return {Object} 282 */ 283 calculateNumberDigits: function(number) { 284 var numberToString = number.toString(); 285 var parts = []; 286 var numberDigits = {}; 287 var operandSymbol = {}; 288 289 if (numberToString.indexOf('.') !== -1) { //decimal 290 parts = numberToString.split('.', 2); 291 numberDigits.integerPart = parseInt(parts[0], 10); 292 numberDigits.decimalPartLength = parts[1].length; 293 numberDigits.decimalPart = parseInt(parts[1], 10); 294 295 operandSymbol.n = parseFloat(number); 296 operandSymbol.i = numberDigits.integerPart; 297 operandSymbol.v = numberDigits.decimalPartLength; 298 operandSymbol.w = numberDigits.decimalPartLength; 299 operandSymbol.f = numberDigits.decimalPart; 300 operandSymbol.t = numberDigits.decimalPart; 301 302 } else { 303 numberDigits.integerPart = number; 304 numberDigits.decimalPartLength = 0; 305 numberDigits.decimalPart = 0; 306 307 operandSymbol.n = parseInt(number, 10); 308 operandSymbol.i = numberDigits.integerPart; 309 operandSymbol.v = 0; 310 operandSymbol.w = 0; 311 operandSymbol.f = 0; 312 operandSymbol.t = 0; 313 314 } 315 return operandSymbol 316 }, 317 318 /** 319 * @private 320 * @param {number|Object} n 321 * @param {Array.<number|Array.<number>>|Object} range 322 * @return {boolean} 323 */ 324 matchRange: function(n, range) { 325 return IString._fncs.matchRangeContinuous(n, range); 326 }, 327 328 /** 329 * @private 330 * @param {Object} rule 331 * @param {number} n 332 * @return {boolean} 333 */ 334 is: function(rule, n) { 335 var left = IString._fncs.getValue(rule[0], n); 336 var right = IString._fncs.getValue(rule[1], n); 337 return left == right; 338 }, 339 340 /** 341 * @private 342 * @param {Object} rule 343 * @param {number} n 344 * @return {boolean} 345 */ 346 isnot: function(rule, n) { 347 return IString._fncs.getValue(rule[0], n) != IString._fncs.getValue(rule[1], n); 348 }, 349 350 /** 351 * @private 352 * @param {Object} rule 353 * @param {number|Object} n 354 * @return {boolean} 355 */ 356 inrange: function(rule, n) { 357 if (typeof(rule[0]) === 'number') { 358 if(typeof(n) === 'object') { 359 return IString._fncs.matchRange(n.n,rule); 360 } 361 return IString._fncs.matchRange(n,rule); 362 } else if (typeof(rule[0]) === 'undefined') { 363 var subrule = IString._fncs.firstPropRule(rule); 364 return IString._fncs[subrule](rule[subrule], n); 365 } else { 366 return IString._fncs.matchRange(IString._fncs.getValue(rule[0], n), rule[1]); 367 } 368 }, 369 /** 370 * @private 371 * @param {Object} rule 372 * @param {number} n 373 * @return {boolean} 374 */ 375 notin: function(rule, n) { 376 return !IString._fncs.matchRange(IString._fncs.getValue(rule[0], n), rule[1]); 377 }, 378 379 /** 380 * @private 381 * @param {Object} rule 382 * @param {number} n 383 * @return {boolean} 384 */ 385 within: function(rule, n) { 386 return IString._fncs.matchRangeContinuous(IString._fncs.getValue(rule[0], n), rule[1]); 387 }, 388 389 /** 390 * @private 391 * @param {Object} rule 392 * @param {number} n 393 * @return {number} 394 */ 395 mod: function(rule, n) { 396 return MathUtils.mod(IString._fncs.getValue(rule[0], n), IString._fncs.getValue(rule[1], n)); 397 }, 398 399 /** 400 * @private 401 * @param {Object} rule 402 * @param {number} n 403 * @return {number} 404 */ 405 n: function(rule, n) { 406 return n; 407 }, 408 409 /** 410 * @private 411 * @param {Object} rule 412 * @param {number|Object} n 413 * @return {boolean} 414 */ 415 or: function(rule, n) { 416 var ruleLength = rule.length; 417 var result, i; 418 for (i=0; i < ruleLength; i++) { 419 result = IString._fncs.getValue(rule[i], n); 420 if (result) { 421 return true; 422 } 423 } 424 return false; 425 }, 426 /** 427 * @private 428 * @param {Object} rule 429 * @param {number|Object} n 430 * @return {boolean} 431 */ 432 and: function(rule, n) { 433 var ruleLength = rule.length; 434 var result, i; 435 for (i=0; i < ruleLength; i++) { 436 result= IString._fncs.getValue(rule[i], n); 437 if (!result) { 438 return false; 439 } 440 } 441 return true; 442 }, 443 /** 444 * @private 445 * @param {Object} rule 446 * @param {number|Object} n 447 * @return {boolean} 448 */ 449 eq: function(rule, n) { 450 var valueLeft = IString._fncs.getValue(rule[0], n); 451 var valueRight; 452 453 if (typeof(rule[0]) === 'string') { 454 if (typeof(n) === 'object'){ 455 valueRight = n[rule[0]]; 456 if (typeof(rule[1])=== 'number'){ 457 valueRight = IString._fncs.getValue(rule[1], n); 458 } else if (typeof(rule[1])=== 'object' && (IString._fncs.firstPropRule(rule[1]) === "inrange" )){ 459 valueRight = IString._fncs.getValue(rule[1], n); 460 } 461 } 462 } else { 463 if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod 464 valueRight = IString._fncs.getValue(rule[1], valueLeft); 465 } else { 466 valueRight = IString._fncs.getValue(rule[1], n); 467 } 468 } 469 if(typeof(valueRight) === 'boolean') { 470 return (valueRight ? true : false); 471 } else { 472 return (valueLeft == valueRight ? true :false); 473 } 474 }, 475 /** 476 * @private 477 * @param {Object} rule 478 * @param {number|Object} n 479 * @return {boolean} 480 */ 481 neq: function(rule, n) { 482 var valueLeft = IString._fncs.getValue(rule[0], n); 483 var valueRight; 484 var leftRange; 485 var rightRange; 486 487 if (typeof(rule[0]) === 'string') { 488 valueRight = n[rule[0]]; 489 if (typeof(rule[1])=== 'number'){ 490 valueRight = IString._fncs.getValue(rule[1], n); 491 } else if (typeof(rule[1]) === 'object') { 492 leftRange = rule[1][0]; 493 rightRange = rule[1][1]; 494 if (typeof(leftRange) === 'number' && 495 typeof(rightRange) === 'number'){ 496 497 if (valueLeft >= leftRange && valueRight <= rightRange) { 498 return false 499 } else { 500 return true; 501 } 502 } 503 } 504 } else { 505 if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod 506 valueRight = IString._fncs.getValue(rule[1], valueLeft); 507 } else { 508 valueRight = IString._fncs.getValue(rule[1], n); 509 } 510 } 511 512 if(typeof(valueRight) === 'boolean') {//mod 513 return (valueRight? false : true); 514 } else { 515 return (valueLeft !== valueRight ? true :false); 516 } 517 518 } 519 }; 520 521 IString.prototype = { 522 /** 523 * Return the length of this string in characters. This function defers to the regular 524 * Javascript string class in order to perform the length function. Please note that this 525 * method is a real method, whereas the length property of Javascript strings is 526 * implemented by native code and appears as a property.<p> 527 * 528 * Example: 529 * 530 * <pre> 531 * var str = new IString("this is a string"); 532 * console.log("String is " + str._length() + " characters long."); 533 * </pre> 534 * @private 535 */ 536 _length: function () { 537 return this.str.length; 538 }, 539 540 /** 541 * Format this string instance as a message, replacing the parameters with 542 * the given values.<p> 543 * 544 * The string can contain any text that a regular Javascript string can 545 * contain. Replacement parameters have the syntax: 546 * 547 * <pre> 548 * {name} 549 * </pre> 550 * 551 * Where "name" can be any string surrounded by curly brackets. The value of 552 * "name" is taken from the parameters argument.<p> 553 * 554 * Example: 555 * 556 * <pre> 557 * var str = new IString("There are {num} objects."); 558 * console.log(str.format({ 559 * num: 12 560 * }); 561 * </pre> 562 * 563 * Would give the output: 564 * 565 * <pre> 566 * There are 12 objects. 567 * </pre> 568 * 569 * If a property is missing from the parameter block, the replacement 570 * parameter substring is left untouched in the string, and a different 571 * set of parameters may be applied a second time. This way, different 572 * parts of the code may format different parts of the message that they 573 * happen to know about.<p> 574 * 575 * Example: 576 * 577 * <pre> 578 * var str = new IString("There are {num} objects in the {container}."); 579 * console.log(str.format({ 580 * num: 12 581 * }); 582 * </pre> 583 * 584 * Would give the output:<p> 585 * 586 * <pre> 587 * There are 12 objects in the {container}. 588 * </pre> 589 * 590 * The result can then be formatted again with a different parameter block that 591 * specifies a value for the container property. 592 * 593 * @param params a Javascript object containing values for the replacement 594 * parameters in the current string 595 * @return a new IString instance with as many replacement parameters filled 596 * out as possible with real values. 597 */ 598 format: function (params) { 599 var formatted = this.str; 600 if (params) { 601 var regex; 602 for (var p in params) { 603 if (typeof(params[p]) !== 'undefined') { 604 regex = new RegExp("\{"+p+"\}", "g"); 605 formatted = formatted.replace(regex, params[p]); 606 } 607 } 608 } 609 return formatted.toString(); 610 }, 611 612 /** @private */ 613 _testChoice: function(index, limit) { 614 var numberDigits = {}; 615 var operandValue = {}; 616 617 switch (typeof(index)) { 618 case 'number': 619 operandValue = IString._fncs.calculateNumberDigits(index); 620 621 if (limit.substring(0,2) === "<=") { 622 limit = parseFloat(limit.substring(2)); 623 return operandValue.n <= limit; 624 } else if (limit.substring(0,2) === ">=") { 625 limit = parseFloat(limit.substring(2)); 626 return operandValue.n >= limit; 627 } else if (limit.charAt(0) === "<") { 628 limit = parseFloat(limit.substring(1)); 629 return operandValue.n < limit; 630 } else if (limit.charAt(0) === ">") { 631 limit = parseFloat(limit.substring(1)); 632 return operandValue.n > limit; 633 } else { 634 this.locale = this.locale || new Locale(this.localeSpec); 635 switch (limit) { 636 case "zero": 637 case "one": 638 case "two": 639 case "few": 640 case "many": 641 // CLDR locale-dependent number classes 642 var ruleset = ilib.data["plurals_" + this.locale.getLanguage()]|| IString.plurals_default; 643 if (ruleset) { 644 var rule = ruleset[limit]; 645 return IString._fncs.getValue(rule, operandValue); 646 } 647 break; 648 case "": 649 case "other": 650 // matches anything 651 return true; 652 default: 653 var dash = limit.indexOf("-"); 654 if (dash !== -1) { 655 // range 656 var start = limit.substring(0, dash); 657 var end = limit.substring(dash+1); 658 return operandValue.n >= parseInt(start, 10) && operandValue.n <= parseInt(end, 10); 659 } else { 660 return operandValue.n === parseInt(limit, 10); 661 } 662 } 663 } 664 break; 665 case 'boolean': 666 return (limit === "true" && index === true) || (limit === "false" && index === false); 667 668 case 'string': 669 var regexp = new RegExp(limit, "i"); 670 return regexp.test(index); 671 672 case 'object': 673 throw "syntax error: formatChoice parameter for the argument index cannot be an object"; 674 } 675 676 return false; 677 }, 678 679 /** 680 * Format a string as one of a choice of strings dependent on the value of 681 * a particular argument index or array of indices.<p> 682 * 683 * The syntax of the choice string is as follows. The string contains a 684 * series of choices separated by a vertical bar character "|". Each choice 685 * has a value or range of values to match followed by a hash character "#" 686 * followed by the string to use if the variable matches the criteria.<p> 687 * 688 * Example string: 689 * 690 * <pre> 691 * var num = 2; 692 * var str = new IString("0#There are no objects.|1#There is one object.|2#There are {number} objects."); 693 * console.log(str.formatChoice(num, { 694 * number: num 695 * })); 696 * </pre> 697 * 698 * Gives the output: 699 * 700 * <pre> 701 * "There are 2 objects." 702 * </pre> 703 * 704 * The strings to format may contain replacement variables that will be formatted 705 * using the format() method above and the params argument as a source of values 706 * to use while formatting those variables.<p> 707 * 708 * If the criterion for a particular choice is empty, that choice will be used 709 * as the default one for use when none of the other choice's criteria match.<p> 710 * 711 * Example string: 712 * 713 * <pre> 714 * var num = 22; 715 * var str = new IString("0#There are no objects.|1#There is one object.|#There are {number} objects."); 716 * console.log(str.formatChoice(num, { 717 * number: num 718 * })); 719 * </pre> 720 * 721 * Gives the output: 722 * 723 * <pre> 724 * "There are 22 objects." 725 * </pre> 726 * 727 * If multiple choice patterns can match a given argument index, the first one 728 * encountered in the string will be used. If no choice patterns match the 729 * argument index, then the default choice will be used. If there is no default 730 * choice defined, then this method will return an empty string.<p> 731 * 732 * <b>Special Syntax</b><p> 733 * 734 * For any choice format string, all of the patterns in the string should be 735 * of a single type: numeric, boolean, or string/regexp. The type of the 736 * patterns is determined by the type of the argument index parameter.<p> 737 * 738 * If the argument index is numeric, then some special syntax can be used 739 * in the patterns to match numeric ranges.<p> 740 * 741 * <ul> 742 * <li><i>>x</i> - match any number that is greater than x 743 * <li><i>>=x</i> - match any number that is greater than or equal to x 744 * <li><i><x</i> - match any number that is less than x 745 * <li><i><=x</i> - match any number that is less than or equal to x 746 * <li><i>start-end</i> - match any number in the range [start,end) 747 * <li><i>zero</i> - match any number in the class "zero". (See below for 748 * a description of number classes.) 749 * <li><i>one</i> - match any number in the class "one" 750 * <li><i>two</i> - match any number in the class "two" 751 * <li><i>few</i> - match any number in the class "few" 752 * <li><i>many</i> - match any number in the class "many" 753 * <li><i>other</i> - match any number in the other or default class 754 * </ul> 755 * 756 * A number class defines a set of numbers that receive a particular syntax 757 * in the strings. For example, in Slovenian, integers ending in the digit 758 * "1" are in the "one" class, including 1, 21, 31, ... 101, 111, etc. 759 * Similarly, integers ending in the digit "2" are in the "two" class. 760 * Integers ending in the digits "3" or "4" are in the "few" class, and 761 * every other integer is handled by the default string.<p> 762 * 763 * The definition of what numbers are included in a class is locale-dependent. 764 * They are defined in the data file plurals.json. If your string is in a 765 * different locale than the default for ilib, you should call the setLocale() 766 * method of the string instance before calling this method.<p> 767 * 768 * <b>Other Pattern Types</b><p> 769 * 770 * If the argument index is a boolean, the string values "true" and "false" 771 * may appear as the choice patterns.<p> 772 * 773 * If the argument index is of type string, then the choice patterns may contain 774 * regular expressions, or static strings as degenerate regexps.<p> 775 * 776 * <b>Multiple Indexes</b><p> 777 * 778 * If you have 2 or more indexes to format into a string, you can pass them as 779 * an array. When you do that, the patterns to match should be a comma-separate 780 * list of patterns as per the rules above.<p> 781 * 782 * Example string: 783 * 784 * <pre> 785 * var str = new IString("zero,zero#There are no objects on zero pages.|one,one#There is 1 object on 1 page.|other,one#There are {number} objects on 1 page.|#There are {number} objects on {pages} pages."); 786 * var num = 4, pages = 1; 787 * console.log(str.formatChoice([num, pages], { 788 * number: num, 789 * pages: pages 790 * })); 791 * </pre> 792 * 793 * Gives the output:<p> 794 * 795 * <pre> 796 * "There are 4 objects on 1 page." 797 * </pre> 798 * 799 * Note that when there is a single index, you would typically leave the pattern blank to 800 * indicate the default choice. When there are multiple indices, sometimes one of the 801 * patterns has to be the default case when the other is not. Rather than leaving one or 802 * more of the patterns blank with commas that look out-of-place in the middle of it, you 803 * can use the word "other" to indicate a match with the default or other choice. The above example 804 * shows the use of the "other" pattern. That said, you are allowed to leave the pattern 805 * blank if you so choose. In the example above, the pattern for the third string could 806 * easily have been written as ",one" instead of "other,one" and the result will be the same. 807 * 808 * @param {*|Array.<*>} argIndex The index into the choice array of the current parameter, 809 * or an array of indices 810 * @param {Object} params The hash of parameter values that replace the replacement 811 * variables in the string 812 * @throws "syntax error in choice format pattern: " if there is a syntax error 813 * @return {string} the formatted string 814 */ 815 formatChoice: function(argIndex, params) { 816 var choices = this.str.split("|"); 817 var limits = []; 818 var strings = []; 819 var i; 820 var parts; 821 var limit; 822 var result = undefined; 823 var defaultCase = ""; 824 825 if (this.str.length === 0) { 826 // nothing to do 827 return ""; 828 } 829 830 // first parse all the choices 831 for (i = 0; i < choices.length; i++) { 832 parts = choices[i].split("#"); 833 if (parts.length > 2) { 834 limits[i] = parts[0]; 835 parts = parts.shift(); 836 strings[i] = parts.join("#"); 837 } else if (parts.length === 2) { 838 limits[i] = parts[0]; 839 strings[i] = parts[1]; 840 } else { 841 // syntax error 842 throw "syntax error in choice format pattern: " + choices[i]; 843 } 844 } 845 846 var args = (ilib.isArray(argIndex)) ? argIndex : [argIndex]; 847 848 // then apply the argument index (or indices) 849 for (i = 0; i < limits.length; i++) { 850 if (limits[i].length === 0) { 851 // this is default case 852 defaultCase = new IString(strings[i]); 853 } else { 854 var limitsArr = (limits[i].indexOf(",") > -1) ? limits[i].split(",") : [limits[i]]; 855 856 var applicable = true; 857 for (var j = 0; applicable && j < args.length && j < limitsArr.length; j++) { 858 applicable = this._testChoice(args[j], limitsArr[j]); 859 } 860 861 if (applicable) { 862 result = new IString(strings[i]); 863 i = limits.length; 864 } 865 } 866 } 867 868 if (!result) { 869 result = defaultCase || new IString(""); 870 } 871 872 result = result.format(params); 873 874 return result.toString(); 875 }, 876 877 // delegates 878 /** 879 * Same as String.toString() 880 * @return {string} this instance as regular Javascript string 881 */ 882 toString: function () { 883 return this.str.toString(); 884 }, 885 886 /** 887 * Same as String.valueOf() 888 * @return {string} this instance as a regular Javascript string 889 */ 890 valueOf: function () { 891 return this.str.valueOf(); 892 }, 893 894 /** 895 * Same as String.charAt() 896 * @param {number} index the index of the character being sought 897 * @return {IString} the character at the given index 898 */ 899 charAt: function(index) { 900 return new IString(this.str.charAt(index)); 901 }, 902 903 /** 904 * Same as String.charCodeAt(). This only reports on 905 * 2-byte UCS-2 Unicode values, and does not take into 906 * account supplementary characters encoded in UTF-16. 907 * If you would like to take account of those characters, 908 * use codePointAt() instead. 909 * @param {number} index the index of the character being sought 910 * @return {number} the character code of the character at the 911 * given index in the string 912 */ 913 charCodeAt: function(index) { 914 return this.str.charCodeAt(index); 915 }, 916 917 /** 918 * Same as String.concat() 919 * @param {string} strings strings to concatenate to the current one 920 * @return {IString} a concatenation of the given strings 921 */ 922 concat: function(strings) { 923 return new IString(this.str.concat(strings)); 924 }, 925 926 /** 927 * Same as String.indexOf() 928 * @param {string} searchValue string to search for 929 * @param {number} start index into the string to start searching, or 930 * undefined to search the entire string 931 * @return {number} index into the string of the string being sought, 932 * or -1 if the string is not found 933 */ 934 indexOf: function(searchValue, start) { 935 return this.str.indexOf(searchValue, start); 936 }, 937 938 /** 939 * Same as String.lastIndexOf() 940 * @param {string} searchValue string to search for 941 * @param {number} start index into the string to start searching, or 942 * undefined to search the entire string 943 * @return {number} index into the string of the string being sought, 944 * or -1 if the string is not found 945 */ 946 lastIndexOf: function(searchValue, start) { 947 return this.str.lastIndexOf(searchValue, start); 948 }, 949 950 /** 951 * Same as String.match() 952 * @param {string} regexp the regular expression to match 953 * @return {Array.<string>} an array of matches 954 */ 955 match: function(regexp) { 956 return this.str.match(regexp); 957 }, 958 959 /** 960 * Same as String.replace() 961 * @param {string} searchValue a regular expression to search for 962 * @param {string} newValue the string to replace the matches with 963 * @return {IString} a new string with all the matches replaced 964 * with the new value 965 */ 966 replace: function(searchValue, newValue) { 967 return new IString(this.str.replace(searchValue, newValue)); 968 }, 969 970 /** 971 * Same as String.search() 972 * @param {string} regexp the regular expression to search for 973 * @return {number} position of the match, or -1 for no match 974 */ 975 search: function(regexp) { 976 return this.str.search(regexp); 977 }, 978 979 /** 980 * Same as String.slice() 981 * @param {number} start first character to include in the string 982 * @param {number} end include all characters up to, but not including 983 * the end character 984 * @return {IString} a slice of the current string 985 */ 986 slice: function(start, end) { 987 return new IString(this.str.slice(start, end)); 988 }, 989 990 /** 991 * Same as String.split() 992 * @param {string} separator regular expression to match to find 993 * separations between the parts of the text 994 * @param {number} limit maximum number of items in the final 995 * output array. Any items beyond that limit will be ignored. 996 * @return {Array.<string>} the parts of the current string split 997 * by the separator 998 */ 999 split: function(separator, limit) { 1000 return this.str.split(separator, limit); 1001 }, 1002 1003 /** 1004 * Same as String.substr() 1005 * @param {number} start the index of the character that should 1006 * begin the returned substring 1007 * @param {number} length the number of characters to return after 1008 * the start character. 1009 * @return {IString} the requested substring 1010 */ 1011 substr: function(start, length) { 1012 var plat = ilib._getPlatform(); 1013 if (plat === "qt" || plat === "rhino" || plat === "trireme") { 1014 // qt and rhino have a broken implementation of substr(), so 1015 // work around it 1016 if (typeof(length) === "undefined") { 1017 length = this.str.length - start; 1018 } 1019 } 1020 return new IString(this.str.substr(start, length)); 1021 }, 1022 1023 /** 1024 * Same as String.substring() 1025 * @param {number} from the index of the character that should 1026 * begin the returned substring 1027 * @param {number} to the index where to stop the extraction. If 1028 * omitted, extracts the rest of the string 1029 * @return {IString} the requested substring 1030 */ 1031 substring: function(from, to) { 1032 return this.str.substring(from, to); 1033 }, 1034 1035 /** 1036 * Same as String.toLowerCase(). Note that this method is 1037 * not locale-sensitive. 1038 * @return {IString} a string with the first character 1039 * lower-cased 1040 */ 1041 toLowerCase: function() { 1042 return this.str.toLowerCase(); 1043 }, 1044 1045 /** 1046 * Same as String.toUpperCase(). Note that this method is 1047 * not locale-sensitive. Use toLocaleUpperCase() instead 1048 * to get locale-sensitive behaviour. 1049 * @return {IString} a string with the first character 1050 * upper-cased 1051 */ 1052 toUpperCase: function() { 1053 return this.str.toUpperCase(); 1054 }, 1055 1056 /** 1057 * Convert the character or the surrogate pair at the given 1058 * index into the string to a Unicode UCS-4 code point. 1059 * @protected 1060 * @param {number} index index into the string 1061 * @return {number} code point of the character at the 1062 * given index into the string 1063 */ 1064 _toCodePoint: function (index) { 1065 return IString.toCodePoint(this.str, index); 1066 }, 1067 1068 /** 1069 * Call the callback with each character in the string one at 1070 * a time, taking care to step through the surrogate pairs in 1071 * the UTF-16 encoding properly.<p> 1072 * 1073 * The standard Javascript String's charAt() method only 1074 * returns a particular 16-bit character in the 1075 * UTF-16 encoding scheme. 1076 * If the index to charAt() is pointing to a low- or 1077 * high-surrogate character, 1078 * it will return the surrogate character rather 1079 * than the the character 1080 * in the supplementary planes that the two surrogates together 1081 * encode. This function will call the callback with the full 1082 * character, making sure to join two 1083 * surrogates into one character in the supplementary planes 1084 * where necessary.<p> 1085 * 1086 * @param {function(string)} callback a callback function to call with each 1087 * full character in the current string 1088 */ 1089 forEach: function(callback) { 1090 if (typeof(callback) === 'function') { 1091 var it = this.charIterator(); 1092 while (it.hasNext()) { 1093 callback(it.next()); 1094 } 1095 } 1096 }, 1097 1098 /** 1099 * Call the callback with each numeric code point in the string one at 1100 * a time, taking care to step through the surrogate pairs in 1101 * the UTF-16 encoding properly.<p> 1102 * 1103 * The standard Javascript String's charCodeAt() method only 1104 * returns information about a particular 16-bit character in the 1105 * UTF-16 encoding scheme. 1106 * If the index to charCodeAt() is pointing to a low- or 1107 * high-surrogate character, 1108 * it will return the code point of the surrogate character rather 1109 * than the code point of the character 1110 * in the supplementary planes that the two surrogates together 1111 * encode. This function will call the callback with the full 1112 * code point of each character, making sure to join two 1113 * surrogates into one code point in the supplementary planes.<p> 1114 * 1115 * @param {function(string)} callback a callback function to call with each 1116 * code point in the current string 1117 */ 1118 forEachCodePoint: function(callback) { 1119 if (typeof(callback) === 'function') { 1120 var it = this.iterator(); 1121 while (it.hasNext()) { 1122 callback(it.next()); 1123 } 1124 } 1125 }, 1126 1127 /** 1128 * Return an iterator that will step through all of the characters 1129 * in the string one at a time and return their code points, taking 1130 * care to step through the surrogate pairs in UTF-16 encoding 1131 * properly.<p> 1132 * 1133 * The standard Javascript String's charCodeAt() method only 1134 * returns information about a particular 16-bit character in the 1135 * UTF-16 encoding scheme. 1136 * If the index is pointing to a low- or high-surrogate character, 1137 * it will return a code point of the surrogate character rather 1138 * than the code point of the character 1139 * in the supplementary planes that the two surrogates together 1140 * encode.<p> 1141 * 1142 * The iterator instance returned has two methods, hasNext() which 1143 * returns true if the iterator has more code points to iterate through, 1144 * and next() which returns the next code point as a number.<p> 1145 * 1146 * @return {Object} an iterator 1147 * that iterates through all the code points in the string 1148 */ 1149 iterator: function() { 1150 /** 1151 * @constructor 1152 */ 1153 function _iterator (istring) { 1154 this.index = 0; 1155 this.hasNext = function () { 1156 return (this.index < istring.str.length); 1157 }; 1158 this.next = function () { 1159 if (this.index < istring.str.length) { 1160 var num = istring._toCodePoint(this.index); 1161 this.index += ((num > 0xFFFF) ? 2 : 1); 1162 } else { 1163 num = -1; 1164 } 1165 return num; 1166 }; 1167 }; 1168 return new _iterator(this); 1169 }, 1170 1171 /** 1172 * Return an iterator that will step through all of the characters 1173 * in the string one at a time, taking 1174 * care to step through the surrogate pairs in UTF-16 encoding 1175 * properly.<p> 1176 * 1177 * The standard Javascript String's charAt() method only 1178 * returns information about a particular 16-bit character in the 1179 * UTF-16 encoding scheme. 1180 * If the index is pointing to a low- or high-surrogate character, 1181 * it will return that surrogate character rather 1182 * than the surrogate pair which represents a character 1183 * in the supplementary planes.<p> 1184 * 1185 * The iterator instance returned has two methods, hasNext() which 1186 * returns true if the iterator has more characters to iterate through, 1187 * and next() which returns the next character.<p> 1188 * 1189 * @return {Object} an iterator 1190 * that iterates through all the characters in the string 1191 */ 1192 charIterator: function() { 1193 /** 1194 * @constructor 1195 */ 1196 function _chiterator (istring) { 1197 this.index = 0; 1198 this.hasNext = function () { 1199 return (this.index < istring.str.length); 1200 }; 1201 this.next = function () { 1202 var ch; 1203 if (this.index < istring.str.length) { 1204 ch = istring.str.charAt(this.index); 1205 if (IString._isSurrogate(ch) && 1206 this.index+1 < istring.str.length && 1207 IString._isSurrogate(istring.str.charAt(this.index+1))) { 1208 this.index++; 1209 ch += istring.str.charAt(this.index); 1210 } 1211 this.index++; 1212 } 1213 return ch; 1214 }; 1215 }; 1216 return new _chiterator(this); 1217 }, 1218 1219 /** 1220 * Return the code point at the given index when the string is viewed 1221 * as an array of code points. If the index is beyond the end of the 1222 * array of code points or if the index is negative, -1 is returned. 1223 * @param {number} index index of the code point 1224 * @return {number} code point of the character at the given index into 1225 * the string 1226 */ 1227 codePointAt: function (index) { 1228 if (index < 0) { 1229 return -1; 1230 } 1231 var count, 1232 it = this.iterator(), 1233 ch; 1234 for (count = index; count >= 0 && it.hasNext(); count--) { 1235 ch = it.next(); 1236 } 1237 return (count < 0) ? ch : -1; 1238 }, 1239 1240 /** 1241 * Set the locale to use when processing choice formats. The locale 1242 * affects how number classes are interpretted. In some cultures, 1243 * the limit "few" maps to "any integer that ends in the digits 2 to 9" and 1244 * in yet others, "few" maps to "any integer that ends in the digits 1245 * 3 or 4". 1246 * @param {Locale|string} locale locale to use when processing choice 1247 * formats with this string 1248 * @param {boolean=} sync [optional] whether to load the locale data synchronously 1249 * or not 1250 * @param {Object=} loadParams [optional] parameters to pass to the loader function 1251 * @param {function(*)=} onLoad [optional] function to call when the loading is done 1252 */ 1253 setLocale: function (locale, sync, loadParams, onLoad) { 1254 if (typeof(locale) === 'object') { 1255 this.locale = locale; 1256 } else { 1257 this.localeSpec = locale; 1258 this.locale = new Locale(locale); 1259 } 1260 1261 IString.loadPlurals(typeof(sync) !== 'undefined' ? sync : true, this.locale, loadParams, onLoad); 1262 }, 1263 1264 /** 1265 * Return the locale to use when processing choice formats. The locale 1266 * affects how number classes are interpretted. In some cultures, 1267 * the limit "few" maps to "any integer that ends in the digits 2 to 9" and 1268 * in yet others, "few" maps to "any integer that ends in the digits 1269 * 3 or 4". 1270 * @return {string} localespec to use when processing choice 1271 * formats with this string 1272 */ 1273 getLocale: function () { 1274 return (this.locale ? this.locale.getSpec() : this.localeSpec) || ilib.getLocale(); 1275 }, 1276 1277 /** 1278 * Return the number of code points in this string. This may be different 1279 * than the number of characters, as the UTF-16 encoding that Javascript 1280 * uses for its basis returns surrogate pairs separately. Two 2-byte 1281 * surrogate characters together make up one character/code point in 1282 * the supplementary character planes. If your string contains no 1283 * characters in the supplementary planes, this method will return the 1284 * same thing as the length() method. 1285 * @return {number} the number of code points in this string 1286 */ 1287 codePointLength: function () { 1288 if (this.cpLength === -1) { 1289 var it = this.iterator(); 1290 this.cpLength = 0; 1291 while (it.hasNext()) { 1292 this.cpLength++; 1293 it.next(); 1294 }; 1295 } 1296 return this.cpLength; 1297 } 1298 }; 1299 1300 module.exports = IString; 1301