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