1 /*
  2  * IString.js - ilib string subclass definition
  3  *
  4  * Copyright © 2012-2015, 2018, 2021-2022 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("../index.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 = String(string); // copy it
 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         var exponentialNum = number.toExponential();
290         var exponentialIndex = exponentialNum.indexOf("e");
291         if (exponentialIndex !== -1) {
292             operandSymbol.c = parseInt(exponentialNum[exponentialIndex+2]);
293             operandSymbol.e = parseInt(exponentialNum[exponentialIndex+2]);
294         } else {
295             operandSymbol.c = 0;
296             operandSymbol.e = 0;
297         }
298 
299         if (numberToString.indexOf('.') !== -1) { //decimal
300             parts = numberToString.split('.', 2);
301             numberDigits.integerPart = parseInt(parts[0], 10);
302             numberDigits.decimalPartLength = parts[1].length;
303             numberDigits.decimalPart = parseInt(parts[1], 10);
304 
305             operandSymbol.n = parseFloat(number);
306             operandSymbol.i = numberDigits.integerPart;
307             operandSymbol.v = numberDigits.decimalPartLength;
308             operandSymbol.w = numberDigits.decimalPartLength;
309             operandSymbol.f = numberDigits.decimalPart;
310             operandSymbol.t = numberDigits.decimalPart;
311 
312         } else {
313             numberDigits.integerPart = number;
314             numberDigits.decimalPartLength = 0;
315             numberDigits.decimalPart = 0;
316 
317             operandSymbol.n = parseInt(number, 10);
318             operandSymbol.i = numberDigits.integerPart;
319             operandSymbol.v = 0;
320             operandSymbol.w = 0;
321             operandSymbol.f = 0;
322             operandSymbol.t = 0;
323 
324         }
325         return operandSymbol
326     },
327 
328     /**
329      * @private
330      * @param {number|Object} n
331      * @param {Array.<number|Array.<number>>|Object} range
332      * @return {boolean}
333      */
334     matchRange: function(n, range) {
335         return IString._fncs.matchRangeContinuous(n, range);
336     },
337 
338     /**
339      * @private
340      * @param {Object} rule
341      * @param {number} n
342      * @return {boolean}
343      */
344     is: function(rule, n) {
345         var left = IString._fncs.getValue(rule[0], n);
346         var right = IString._fncs.getValue(rule[1], n);
347         return left === right;
348     },
349 
350     /**
351      * @private
352      * @param {Object} rule
353      * @param {number} n
354      * @return {boolean}
355      */
356     isnot: function(rule, n) {
357         return IString._fncs.getValue(rule[0], n) !== IString._fncs.getValue(rule[1], n);
358     },
359 
360     /**
361      * @private
362      * @param {Object} rule
363      * @param {number|Object} n
364      * @return {boolean}
365      */
366     inrange: function(rule, n) {
367         if (typeof(rule[0]) === 'number') {
368             if(typeof(n) === 'object') {
369                 return IString._fncs.matchRange(n.n,rule);
370             }
371             return IString._fncs.matchRange(n,rule);
372         } else if (typeof(rule[0]) === 'undefined') {
373             var subrule = IString._fncs.firstPropRule(rule);
374             return IString._fncs[subrule](rule[subrule], n);
375         } else {
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     notin: function(rule, n) {
386         return !IString._fncs.matchRange(IString._fncs.getValue(rule[0], n), rule[1]);
387     },
388 
389     /**
390      * @private
391      * @param {Object} rule
392      * @param {number} n
393      * @return {boolean}
394      */
395     within: function(rule, n) {
396         return IString._fncs.matchRangeContinuous(IString._fncs.getValue(rule[0], n), rule[1]);
397     },
398 
399     /**
400      * @private
401      * @param {Object} rule
402      * @param {number} n
403      * @return {number}
404      */
405     mod: function(rule, n) {
406         return MathUtils.mod(IString._fncs.getValue(rule[0], n), IString._fncs.getValue(rule[1], n));
407     },
408 
409     /**
410      * @private
411      * @param {Object} rule
412      * @param {number} n
413      * @return {number}
414      */
415     n: function(rule, n) {
416         return n;
417     },
418 
419     /**
420      * @private
421      * @param {Object} rule
422      * @param {number|Object} n
423      * @return {boolean}
424      */
425     or: function(rule, n) {
426         var ruleLength = rule.length;
427         var result, i;
428         for (i=0; i < ruleLength; i++) {
429             result = IString._fncs.getValue(rule[i], n);
430             if (result) {
431                 return true;
432             }
433         }
434         return false;
435     },
436     /**
437      * @private
438      * @param {Object} rule
439      * @param {number|Object} n
440      * @return {boolean}
441      */
442     and: function(rule, n) {
443         var ruleLength = rule.length;
444         var result, i;
445         for (i=0; i < ruleLength; i++) {
446             result= IString._fncs.getValue(rule[i], n);
447             if (!result) {
448                 return false;
449             }
450         }
451         return true;
452     },
453     /**
454      * @private
455      * @param {Object} rule
456      * @param {number|Object} n
457      * @return {boolean}
458      */
459     eq: function(rule, n) {
460         var valueLeft = IString._fncs.getValue(rule[0], n);
461         var valueRight;
462 
463         if (typeof(rule[0]) === 'string') {
464             if (typeof(n) === 'object'){
465                 valueRight = n[rule[0]];
466                 if (typeof(rule[1])=== 'number'){
467                     valueRight = IString._fncs.getValue(rule[1], n);
468                 } else if (typeof(rule[1])=== 'object' && (IString._fncs.firstPropRule(rule[1]) === "inrange" )){
469                     valueRight = IString._fncs.getValue(rule[1], n);
470                 }
471             }
472         } else {
473             if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod
474                 valueRight = IString._fncs.getValue(rule[1], valueLeft);
475             } else {
476                 valueRight = IString._fncs.getValue(rule[1], n);
477             }
478         }
479         if(typeof(valueRight) === 'boolean') {
480             return (valueRight ? true : false);
481         } else {
482             return (valueLeft === valueRight ? true :false);
483         }
484     },
485     /**
486      * @private
487      * @param {Object} rule
488      * @param {number|Object} n
489      * @return {boolean}
490      */
491     neq: function(rule, n) {
492         var valueLeft = IString._fncs.getValue(rule[0], n);
493         var valueRight;
494         var leftRange;
495         var rightRange;
496 
497         if (typeof(rule[0]) === 'string') {
498             valueRight = n[rule[0]];
499             if (typeof(rule[1])=== 'number'){
500                 valueRight = IString._fncs.getValue(rule[1], n);
501             } else if (typeof(rule[1]) === 'object') {
502                 leftRange = rule[1][0];
503                 rightRange =  rule[1][1];
504                 if (typeof(leftRange) === 'number' &&
505                     typeof(rightRange) === 'number'){
506 
507                     if (valueLeft >= leftRange && valueRight <= rightRange) {
508                         return false
509                     } else {
510                         return true;
511                     }
512                 }
513             }
514         } else {
515             if (IString._fncs.firstPropRule(rule[1]) === "inrange") { // mod
516                 valueRight = IString._fncs.getValue(rule[1], valueLeft);
517             } else {
518                 valueRight = IString._fncs.getValue(rule[1], n);
519             }
520         }
521 
522         if(typeof(valueRight) === 'boolean') {//mod
523             return (valueRight? false : true);
524         } else {
525             return (valueLeft !== valueRight ? true :false);
526         }
527 
528     }
529 };
530 
531 IString.prototype = {
532     /**
533      * Return the length of this string in characters. This function defers to the regular
534      * Javascript string class in order to perform the length function. Please note that this
535      * method is a real method, whereas the length property of Javascript strings is
536      * implemented by native code and appears as a property.<p>
537      *
538      * Example:
539      *
540      * <pre>
541      * var str = new IString("this is a string");
542      * console.log("String is " + str._length() + " characters long.");
543      * </pre>
544      * @private
545      * @deprecated
546      */
547     _length: function () {
548         return this.str.length;
549     },
550 
551     /**
552      * Format this string instance as a message, replacing the parameters with
553      * the given values.<p>
554      *
555      * The string can contain any text that a regular Javascript string can
556      * contain. Replacement parameters have the syntax:
557      *
558      * <pre>
559      * {name}
560      * </pre>
561      *
562      * Where "name" can be any string surrounded by curly brackets. The value of
563      * "name" is taken from the parameters argument.<p>
564      *
565      * Example:
566      *
567      * <pre>
568      * var str = new IString("There are {num} objects.");
569      * console.log(str.format({
570      *   num: 12
571      * });
572      * </pre>
573      *
574      * Would give the output:
575      *
576      * <pre>
577      * There are 12 objects.
578      * </pre>
579      *
580      * If a property is missing from the parameter block, the replacement
581      * parameter substring is left untouched in the string, and a different
582      * set of parameters may be applied a second time. This way, different
583      * parts of the code may format different parts of the message that they
584      * happen to know about.<p>
585      *
586      * Example:
587      *
588      * <pre>
589      * var str = new IString("There are {num} objects in the {container}.");
590      * console.log(str.format({
591      *   num: 12
592      * });
593      * </pre>
594      *
595      * Would give the output:<p>
596      *
597      * <pre>
598      * There are 12 objects in the {container}.
599      * </pre>
600      *
601      * The result can then be formatted again with a different parameter block that
602      * specifies a value for the container property.
603      *
604      * @param params a Javascript object containing values for the replacement
605      * parameters in the current string
606      * @return a new IString instance with as many replacement parameters filled
607      * out as possible with real values.
608      */
609     format: function (params) {
610         var formatted = this.str;
611         if (params) {
612             var regex;
613             for (var p in params) {
614                 if (typeof(params[p]) !== 'undefined') {
615                     regex = new RegExp("\{"+p+"\}", "g");
616                     formatted = formatted.replace(regex, params[p]);
617                 }
618             }
619         }
620         return formatted.toString();
621     },
622 
623     /** @private */
624     _testChoice: function(index, limit) {
625         var operandValue = {};
626 
627         switch (typeof(index)) {
628             case 'number':
629                 operandValue = IString._fncs.calculateNumberDigits(index);
630 
631                 if (limit.substring(0,2) === "<=") {
632                     limit = parseFloat(limit.substring(2));
633                     return operandValue.n <= limit;
634                 } else if (limit.substring(0,2) === ">=") {
635                     limit = parseFloat(limit.substring(2));
636                     return operandValue.n >= limit;
637                 } else if (limit.charAt(0) === "<") {
638                     limit = parseFloat(limit.substring(1));
639                     return operandValue.n < limit;
640                 } else if (limit.charAt(0) === ">") {
641                     limit = parseFloat(limit.substring(1));
642                     return operandValue.n > limit;
643                 } else {
644                     this.locale = this.locale || new Locale(this.localeSpec);
645                     switch (limit) {
646                         case "zero":
647                         case "one":
648                         case "two":
649                         case "few":
650                         case "many":
651                             // CLDR locale-dependent number classes
652                             var ruleset = ilib.data["plurals_" + this.locale.getLanguage()+ "_" + this.locale.getRegion()] || ilib.data["plurals_" + this.locale.getLanguage()]|| IString.plurals_default;
653                             if (ruleset) {
654                                 var rule = ruleset[limit];
655                                 return IString._fncs.getValue(rule, operandValue);
656                             }
657                             break;
658                         case "":
659                         case "other":
660                             // matches anything
661                             return true;
662                         default:
663                             var dash = limit.indexOf("-");
664                             if (dash !== -1) {
665                                 // range
666                                 var start = limit.substring(0, dash);
667                                 var end = limit.substring(dash+1);
668                                 return operandValue.n >= parseInt(start, 10) && operandValue.n <= parseInt(end, 10);
669                             } else {
670                                 return operandValue.n === parseInt(limit, 10);
671                             }
672                     }
673                 }
674                 break;
675             case 'boolean':
676                 return (limit === "true" && index === true) || (limit === "false" && index === false);
677 
678             case 'string':
679                 var regexp = new RegExp(limit, "i");
680                 return regexp.test(index);
681 
682             case 'object':
683                 throw "syntax error: formatChoice parameter for the argument index cannot be an object";
684         }
685 
686         return false;
687     },
688     /** @private */
689     _isIntlPluralAvailable: function(locale) {
690         if (typeof (locale.getVariant()) !== 'undefined'){
691             return false;
692         }
693 
694         if (typeof(Intl) !== 'undefined') {
695             if (ilib._getPlatform() === 'nodejs') {
696                 var version = process.versions["node"];
697                 if (!version) return false;
698                 var majorVersion = version.split(".")[0];
699                 if (Number(majorVersion) >= 10 && (Intl.PluralRules.supportedLocalesOf(locale.getSpec()).length > 0)) {
700                     return true;
701                 }
702                 return false;
703             } else if (Intl.PluralRules.supportedLocalesOf(locale.getSpec()).length > 0) {
704                 return true;
705             } else {
706                 return false;
707             }
708         }
709         return false;
710     },
711 
712     /**
713      * Format a string as one of a choice of strings dependent on the value of
714      * a particular argument index or array of indices.<p>
715      *
716      * The syntax of the choice string is as follows. The string contains a
717      * series of choices separated by a vertical bar character "|". Each choice
718      * has a value or range of values to match followed by a hash character "#"
719      * followed by the string to use if the variable matches the criteria.<p>
720      *
721      * Example string:
722      *
723      * <pre>
724      * var num = 2;
725      * var str = new IString("0#There are no objects.|1#There is one object.|2#There are {number} objects.");
726      * console.log(str.formatChoice(num, {
727      *   number: num
728      * }));
729      * </pre>
730      *
731      * Gives the output:
732      *
733      * <pre>
734      * "There are 2 objects."
735      * </pre>
736      *
737      * The strings to format may contain replacement variables that will be formatted
738      * using the format() method above and the params argument as a source of values
739      * to use while formatting those variables.<p>
740      *
741      * If the criterion for a particular choice is empty, that choice will be used
742      * as the default one for use when none of the other choice's criteria match.<p>
743      *
744      * Example string:
745      *
746      * <pre>
747      * var num = 22;
748      * var str = new IString("0#There are no objects.|1#There is one object.|#There are {number} objects.");
749      * console.log(str.formatChoice(num, {
750      *   number: num
751      * }));
752      * </pre>
753      *
754      * Gives the output:
755      *
756      * <pre>
757      * "There are 22 objects."
758      * </pre>
759      *
760      * If multiple choice patterns can match a given argument index, the first one
761      * encountered in the string will be used. If no choice patterns match the
762      * argument index, then the default choice will be used. If there is no default
763      * choice defined, then this method will return an empty string.<p>
764      *
765      * <b>Special Syntax</b><p>
766      *
767      * For any choice format string, all of the patterns in the string should be
768      * of a single type: numeric, boolean, or string/regexp. The type of the
769      * patterns is determined by the type of the argument index parameter.<p>
770      *
771      * If the argument index is numeric, then some special syntax can be used
772      * in the patterns to match numeric ranges.<p>
773      *
774      * <ul>
775      * <li><i>>x</i> - match any number that is greater than x
776      * <li><i>>=x</i> - match any number that is greater than or equal to x
777      * <li><i><x</i> - match any number that is less than x
778      * <li><i><=x</i> - match any number that is less than or equal to x
779      * <li><i>start-end</i> - match any number in the range [start,end)
780      * <li><i>zero</i> - match any number in the class "zero". (See below for
781      * a description of number classes.)
782      * <li><i>one</i> - match any number in the class "one"
783      * <li><i>two</i> - match any number in the class "two"
784      * <li><i>few</i> - match any number in the class "few"
785      * <li><i>many</i> - match any number in the class "many"
786      * <li><i>other</i> - match any number in the other or default class
787      * </ul>
788      *
789      * A number class defines a set of numbers that receive a particular syntax
790      * in the strings. For example, in Slovenian, integers ending in the digit
791      * "1" are in the "one" class, including 1, 21, 31, ... 101, 111, etc.
792      * Similarly, integers ending in the digit "2" are in the "two" class.
793      * Integers ending in the digits "3" or "4" are in the "few" class, and
794      * every other integer is handled by the default string.<p>
795      *
796      * The definition of what numbers are included in a class is locale-dependent.
797      * They are defined in the data file plurals.json. If your string is in a
798      * different locale than the default for ilib, you should call the setLocale()
799      * method of the string instance before calling this method.<p>
800      *
801      * <b>Other Pattern Types</b><p>
802      *
803      * If the argument index is a boolean, the string values "true" and "false"
804      * may appear as the choice patterns.<p>
805      *
806      * If the argument index is of type string, then the choice patterns may contain
807      * regular expressions, or static strings as degenerate regexps.<p>
808      *
809      * <b>Multiple Indexes</b><p>
810      *
811      * If you have 2 or more indexes to format into a string, you can pass them as
812      * an array. When you do that, the patterns to match should be a comma-separate
813      * list of patterns as per the rules above.<p>
814      *
815      * Example string:
816      *
817      * <pre>
818      * 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.");
819      * var num = 4, pages = 1;
820      * console.log(str.formatChoice([num, pages], {
821      *   number: num,
822      *   pages: pages
823      * }));
824      * </pre>
825      *
826      * Gives the output:<p>
827      *
828      * <pre>
829      * "There are 4 objects on 1 page."
830      * </pre>
831      *
832      * Note that when there is a single index, you would typically leave the pattern blank to
833      * indicate the default choice. When there are multiple indices, sometimes one of the
834      * patterns has to be the default case when the other is not. Rather than leaving one or
835      * more of the patterns blank with commas that look out-of-place in the middle of it, you
836      * can use the word "other" to indicate a match with the default or other choice. The above example
837      * shows the use of the "other" pattern. That said, you are allowed to leave the pattern
838      * blank if you so choose. In the example above, the pattern for the third string could
839      * easily have been written as ",one" instead of "other,one" and the result will be the same.
840      *
841      * @param {*|Array.<*>} argIndex The index into the choice array of the current parameter,
842      * or an array of indices
843      * @param {Object} params The hash of parameter values that replace the replacement
844      * variables in the string
845      * * @param {boolean} useIntlPlural [optional] true if you are willing to use Intl.PluralRules object
846      * If it is omitted, the default value is true
847      * @throws "syntax error in choice format pattern: " if there is a syntax error
848      * @return {string} the formatted string
849      */
850     formatChoice: function(argIndex, params, useIntlPlural) {
851         var choices = this.str.split("|");
852         var limits = [];
853         var strings = [];
854         var limitsArr = [];
855         var i;
856         var parts;
857         var result = undefined;
858         var defaultCase = "";
859         var checkArgsType;
860         var useIntl = typeof(useIntlPlural) !== 'undefined' ? useIntlPlural : true;
861         if (this.str.length === 0) {
862             // nothing to do
863             return "";
864         }
865 
866         // first parse all the choices
867         for (i = 0; i < choices.length; i++) {
868             parts = choices[i].split("#");
869             if (parts.length > 2) {
870                 limits[i] = parts[0];
871                 parts = parts.shift();
872                 strings[i] = parts.join("#");
873             } else if (parts.length === 2) {
874                 limits[i] = parts[0];
875                 strings[i] = parts[1];
876             } else {
877                 // syntax error
878                 throw "syntax error in choice format pattern: " + choices[i];
879             }
880         }
881 
882         var args = (ilib.isArray(argIndex)) ? argIndex : [argIndex];
883 
884         checkArgsType = args.filter(ilib.bind(this, function(item){
885             if (typeof(item) !== "number") {
886                 return false;
887             }
888             return true;
889         }));
890 
891         if (useIntl && this.intlPlural && (args.length === checkArgsType.length)){
892             this.cateArr = [];
893             for(i = 0; i < args.length;i++) {
894                 var r = this.intlPlural.select(args[i]);
895                 this.cateArr.push(r);
896             }
897             if (args.length === 1) {
898                 var idx = limits.indexOf(this.cateArr[0]);
899                 if (idx == -1) {
900                     idx = limits.indexOf("");
901                 }
902                 result = new IString(strings[idx]);
903             } else {
904                 if (limits.length === 0) {
905                     defaultCase = new IString(strings[i]);
906                 } else {
907                     this.findOne = false;
908 
909                     for(i = 0; !this.findOne && i < limits.length; i++){
910                         limitsArr = (limits[i].indexOf(",") > -1) ? limits[i].split(",") : [limits[i]];
911 
912                         if (limitsArr.length > 1 && (limitsArr.length < this.cateArr.length)){
913                             this.cateArr = this.cateArr.slice(0,limitsArr.length);
914                         }
915                         limitsArr = limitsArr.map(function(item){
916                             return item.trim();
917                         })
918                         limitsArr.filter(ilib.bind(this, function(element, idx, arr){
919                             if (JSON.stringify(arr) === JSON.stringify(this.cateArr)){
920                                 this.number = i;
921                                 this.fineOne = true;
922                             }
923                         }));
924                     }
925                     if (this.number === -1){
926                         this.number = limits.indexOf("");
927                     }
928                     result = new IString(strings[this.number]);
929                 }
930             }
931         } else {
932             // then apply the argument index (or indices)
933             for (i = 0; i < limits.length; i++) {
934                 if (limits[i].length === 0) {
935                     // this is default case
936                     defaultCase = new IString(strings[i]);
937                 } else {
938                     limitsArr = (limits[i].indexOf(",") > -1) ? limits[i].split(",") : [limits[i]];
939 
940                     var applicable = true;
941                     for (var j = 0; applicable && j < args.length && j < limitsArr.length; j++) {
942                         applicable = this._testChoice(args[j], limitsArr[j]);
943                     }
944 
945                     if (applicable) {
946                         result = new IString(strings[i]);
947                         i = limits.length;
948                     }
949                 }
950             }
951         }
952         if (!result) {
953             result = defaultCase || new IString("");
954         }
955 
956         result = result.format(params);
957 
958         return result.toString();
959     },
960 
961     // delegates
962     /**
963      * Same as String.toString()
964      * @return {string} this instance as regular Javascript string
965      */
966     toString: function () {
967         return this.str.toString();
968     },
969 
970     /**
971      * Same as String.valueOf()
972      * @return {string} this instance as a regular Javascript string
973      */
974     valueOf: function () {
975         return this.str.valueOf();
976     },
977 
978     /**
979      * Same as String.charAt()
980      * @param {number} index the index of the character being sought
981      * @return {IString} the character at the given index
982      */
983     charAt: function(index) {
984         return new IString(this.str.charAt(index));
985     },
986 
987     /**
988      * Same as String.charCodeAt(). This only reports on
989      * 2-byte UCS-2 Unicode values, and does not take into
990      * account supplementary characters encoded in UTF-16.
991      * If you would like to take account of those characters,
992      * use codePointAt() instead.
993      * @param {number} index the index of the character being sought
994      * @return {number} the character code of the character at the
995      * given index in the string
996      */
997     charCodeAt: function(index) {
998         return this.str.charCodeAt(index);
999     },
1000 
1001     /**
1002      * Same as String.concat()
1003      * @param {string} strings strings to concatenate to the current one
1004      * @return {IString} a concatenation of the given strings
1005      */
1006     concat: function(strings) {
1007         return new IString(this.str.concat(strings));
1008     },
1009 
1010     /**
1011      * Same as String.indexOf()
1012      * @param {string} searchValue string to search for
1013      * @param {number} start index into the string to start searching, or
1014      * undefined to search the entire string
1015      * @return {number} index into the string of the string being sought,
1016      * or -1 if the string is not found
1017      */
1018     indexOf: function(searchValue, start) {
1019         return this.str.indexOf(searchValue, start);
1020     },
1021 
1022     /**
1023      * Same as String.lastIndexOf()
1024      * @param {string} searchValue string to search for
1025      * @param {number} start index into the string to start searching, or
1026      * undefined to search the entire string
1027      * @return {number} index into the string of the string being sought,
1028      * or -1 if the string is not found
1029      */
1030     lastIndexOf: function(searchValue, start) {
1031         return this.str.lastIndexOf(searchValue, start);
1032     },
1033 
1034     /**
1035      * Same as String.match()
1036      * @param {string} regexp the regular expression to match
1037      * @return {Array.<string>} an array of matches
1038      */
1039     match: function(regexp) {
1040         return this.str.match(regexp);
1041     },
1042 
1043     /**
1044      * Same as String.matchAll()
1045      * @param {string} regexp the regular expression to match
1046      * @return {iterator} an iterator of the matches
1047      */
1048     matchAll: function(regexp) {
1049         return this.str.matchAll(regexp);
1050     },
1051 
1052     /**
1053      * Same as String.replace()
1054      * @param {string} searchValue a regular expression to search for
1055      * @param {string} newValue the string to replace the matches with
1056      * @return {IString} a new string with all the matches replaced
1057      * with the new value
1058      */
1059     replace: function(searchValue, newValue) {
1060         return new IString(this.str.replace(searchValue, newValue));
1061     },
1062 
1063     /**
1064      * Same as String.search()
1065      * @param {string} regexp the regular expression to search for
1066      * @return {number} position of the match, or -1 for no match
1067      */
1068     search: function(regexp) {
1069         return this.str.search(regexp);
1070     },
1071 
1072     /**
1073      * Same as String.slice()
1074      * @param {number} start first character to include in the string
1075      * @param {number} end include all characters up to, but not including
1076      * the end character
1077      * @return {IString} a slice of the current string
1078      */
1079     slice: function(start, end) {
1080         return new IString(this.str.slice(start, end));
1081     },
1082 
1083     /**
1084      * Same as String.split()
1085      * @param {string} separator regular expression to match to find
1086      * separations between the parts of the text
1087      * @param {number} limit maximum number of items in the final
1088      * output array. Any items beyond that limit will be ignored.
1089      * @return {Array.<string>} the parts of the current string split
1090      * by the separator
1091      */
1092     split: function(separator, limit) {
1093         return this.str.split(separator, limit);
1094     },
1095 
1096     /**
1097      * Same as String.substr()
1098      * @param {number} start the index of the character that should
1099      * begin the returned substring
1100      * @param {number} length the number of characters to return after
1101      * the start character.
1102      * @return {IString} the requested substring
1103      */
1104     substr: function(start, length) {
1105         var plat = ilib._getPlatform();
1106         if (plat === "qt" || plat === "rhino" || plat === "trireme") {
1107             // qt and rhino have a broken implementation of substr(), so
1108             // work around it
1109             if (typeof(length) === "undefined") {
1110                 length = this.str.length - start;
1111             }
1112         }
1113         return new IString(this.str.substr(start, length));
1114     },
1115 
1116     /**
1117      * Same as String.substring()
1118      * @param {number} from the index of the character that should
1119      * begin the returned substring
1120      * @param {number} to the index where to stop the extraction. If
1121      * omitted, extracts the rest of the string
1122      * @return {IString} the requested substring
1123      */
1124     substring: function(from, to) {
1125         return this.str.substring(from, to);
1126     },
1127 
1128     /**
1129      * Same as String.toLowerCase(). Note that this method is
1130      * not locale-sensitive.
1131      * @return {IString} a string with the first character
1132      * lower-cased
1133      */
1134     toLowerCase: function() {
1135         return this.str.toLowerCase();
1136     },
1137 
1138     /**
1139      * Same as String.toUpperCase(). Note that this method is
1140      * not locale-sensitive. Use toLocaleUpperCase() instead
1141      * to get locale-sensitive behaviour.
1142      * @return {IString} a string with the first character
1143      * upper-cased
1144      */
1145     toUpperCase: function() {
1146         return this.str.toUpperCase();
1147     },
1148 
1149     /**
1150      * Same as String.endsWith().
1151      * @return {boolean} true if the given characters are found at
1152      * the end of the string, and false otherwise
1153      */
1154     endsWith: function(searchString, length) {
1155         /* (note)length is optional. If it is omitted the default value is the length of string.
1156         *  But If length is omitted, it returns false on QT. (tested on QT 5.12.4 and 5.13.0)
1157         */
1158         if (typeof length === "undefined") {
1159             length = this.str.length;
1160         }
1161         return this.str.endsWith(searchString, length);
1162     },
1163 
1164     /**
1165      * Same as String.startsWith().
1166      * @return {boolean} true if the given characters are found at
1167      * the beginning of the string, and false otherwise
1168      */
1169     startsWith: function(searchString, length) {
1170         return this.str.startsWith(searchString, length);
1171     },
1172 
1173     /**
1174      * Same as String.includes().
1175      * @return {boolean} true if the search string is found anywhere
1176      * with the given string, and false otherwise
1177      */
1178     includes: function(searchString, position) {
1179         return this.str.includes(searchString, position);
1180     },
1181 
1182     /**
1183      * Same as String.normalize(). If this JS engine does not support
1184      * this method, then you can use the NormString class of ilib
1185      * to the same thing (albeit a little slower).
1186      *
1187      * @return {string} the string in normalized form
1188      */
1189     normalize: function(form) {
1190         return this.str.normalize(form);
1191     },
1192 
1193     /**
1194      * Same as String.padEnd().
1195      * @return {string} a string of the specified length with the
1196      * pad string applied at the end of the current string
1197      */
1198     padEnd: function(targetLength, padString) {
1199         return this.str.padEnd(targetLength, padString);
1200     },
1201 
1202     /**
1203      * Same as String.padStart().
1204      * @return {string} a string of the specified length with the
1205      * pad string applied at the end of the current string
1206      */
1207     padStart: function(targetLength, padString) {
1208         return this.str.padStart(targetLength, padString);
1209     },
1210 
1211     /**
1212      * Same as String.repeat().
1213      * @return {string} a new string containing the specified number
1214      * of copies of the given string
1215      */
1216     repeat: function(count) {
1217         return this.str.repeat(count);
1218     },
1219 
1220     /**
1221      * Same as String.toLocaleLowerCase(). If the JS engine does not support this
1222      * method, you can use the ilib CaseMapper class instead.
1223      * @return {string} a new string representing the calling string
1224      * converted to lower case, according to any locale-sensitive
1225      * case mappings
1226      */
1227     toLocaleLowerCase: function(locale) {
1228         return this.str.toLocaleLowerCase(locale);
1229     },
1230 
1231     /**
1232      * Same as String.toLocaleUpperCase(). If the JS engine does not support this
1233      * method, you can use the ilib CaseMapper class instead.
1234      * @return {string} a new string representing the calling string
1235      * converted to upper case, according to any locale-sensitive
1236      * case mappings
1237      */
1238     toLocaleUpperCase: function(locale) {
1239         return this.str.toLocaleUpperCase(locale);
1240     },
1241 
1242     /**
1243      * Same as String.trim().
1244      * @return {string} a new string representing the calling string stripped
1245      * of whitespace from both ends.
1246      */
1247     trim: function() {
1248         return this.str.trim();
1249     },
1250 
1251     /**
1252      * Same as String.trimEnd().
1253      * @return {string} a new string representing the calling string stripped
1254      * of whitespace from its (right) end.
1255      */
1256     trimEnd: function() {
1257         return this.str.trimEnd();
1258     },
1259 
1260     /**
1261      * Same as String.trimRight().
1262      * @return {string} a new string representing the calling string stripped
1263      * of whitespace from its (right) end.
1264      */
1265     trimRight: function() {
1266         return this.str.trimRight();
1267     },
1268 
1269     /**
1270      * Same as String.trimStart().
1271      * @return {string} A new string representing the calling string stripped
1272      * of whitespace from its beginning (left end).
1273      */
1274     trimStart: function() {
1275         return this.str.trimStart();
1276     },
1277 
1278     /**
1279      * Same as String.trimLeft().
1280      * @return {string} A new string representing the calling string stripped
1281      * of whitespace from its beginning (left end).
1282      */
1283     trimLeft: function() {
1284         return this.str.trimLeft();
1285     },
1286 
1287     /**
1288      * Convert the character or the surrogate pair at the given
1289      * index into the string to a Unicode UCS-4 code point.
1290      * @protected
1291      * @param {number} index index into the string
1292      * @return {number} code point of the character at the
1293      * given index into the string
1294      */
1295     _toCodePoint: function (index) {
1296         return IString.toCodePoint(this.str, index);
1297     },
1298 
1299     /**
1300      * Call the callback with each character in the string one at
1301      * a time, taking care to step through the surrogate pairs in
1302      * the UTF-16 encoding properly.<p>
1303      *
1304      * The standard Javascript String's charAt() method only
1305      * returns a particular 16-bit character in the
1306      * UTF-16 encoding scheme.
1307      * If the index to charAt() is pointing to a low- or
1308      * high-surrogate character,
1309      * it will return the surrogate character rather
1310      * than the the character
1311      * in the supplementary planes that the two surrogates together
1312      * encode. This function will call the callback with the full
1313      * character, making sure to join two
1314      * surrogates into one character in the supplementary planes
1315      * where necessary.<p>
1316      *
1317      * @param {function(string)} callback a callback function to call with each
1318      * full character in the current string
1319      */
1320     forEach: function(callback) {
1321         if (typeof(callback) === 'function') {
1322             var it = this.charIterator();
1323             while (it.hasNext()) {
1324                 callback(it.next());
1325             }
1326         }
1327     },
1328 
1329     /**
1330      * Call the callback with each numeric code point in the string one at
1331      * a time, taking care to step through the surrogate pairs in
1332      * the UTF-16 encoding properly.<p>
1333      *
1334      * The standard Javascript String's charCodeAt() method only
1335      * returns information about a particular 16-bit character in the
1336      * UTF-16 encoding scheme.
1337      * If the index to charCodeAt() is pointing to a low- or
1338      * high-surrogate character,
1339      * it will return the code point of the surrogate character rather
1340      * than the code point of the character
1341      * in the supplementary planes that the two surrogates together
1342      * encode. This function will call the callback with the full
1343      * code point of each character, making sure to join two
1344      * surrogates into one code point in the supplementary planes.<p>
1345      *
1346      * @param {function(string)} callback a callback function to call with each
1347      * code point in the current string
1348      */
1349     forEachCodePoint: function(callback) {
1350         if (typeof(callback) === 'function') {
1351             var it = this.iterator();
1352             while (it.hasNext()) {
1353                 callback(it.next());
1354             }
1355         }
1356     },
1357 
1358     /**
1359      * Return an iterator that will step through all of the characters
1360      * in the string one at a time and return their code points, taking
1361      * care to step through the surrogate pairs in UTF-16 encoding
1362      * properly.<p>
1363      *
1364      * The standard Javascript String's charCodeAt() method only
1365      * returns information about a particular 16-bit character in the
1366      * UTF-16 encoding scheme.
1367      * If the index is pointing to a low- or high-surrogate character,
1368      * it will return a code point of the surrogate character rather
1369      * than the code point of the character
1370      * in the supplementary planes that the two surrogates together
1371      * encode.<p>
1372      *
1373      * The iterator instance returned has two methods, hasNext() which
1374      * returns true if the iterator has more code points to iterate through,
1375      * and next() which returns the next code point as a number.<p>
1376      *
1377      * @return {Object} an iterator
1378      * that iterates through all the code points in the string
1379      */
1380     iterator: function() {
1381         /**
1382          * @constructor
1383          */
1384         function _iterator (istring) {
1385             this.index = 0;
1386             this.hasNext = function () {
1387                 return (this.index < istring.str.length);
1388             };
1389             this.next = function () {
1390                 if (this.index < istring.str.length) {
1391                     var num = istring._toCodePoint(this.index);
1392                     this.index += ((num > 0xFFFF) ? 2 : 1);
1393                 } else {
1394                     num = -1;
1395                 }
1396                 return num;
1397             };
1398         };
1399         return new _iterator(this);
1400     },
1401 
1402     /**
1403      * Return an iterator that will step through all of the characters
1404      * in the string one at a time, taking
1405      * care to step through the surrogate pairs in UTF-16 encoding
1406      * properly.<p>
1407      *
1408      * The standard Javascript String's charAt() method only
1409      * returns information about a particular 16-bit character in the
1410      * UTF-16 encoding scheme.
1411      * If the index is pointing to a low- or high-surrogate character,
1412      * it will return that surrogate character rather
1413      * than the surrogate pair which represents a character
1414      * in the supplementary planes.<p>
1415      *
1416      * The iterator instance returned has two methods, hasNext() which
1417      * returns true if the iterator has more characters to iterate through,
1418      * and next() which returns the next character.<p>
1419      *
1420      * @return {Object} an iterator
1421      * that iterates through all the characters in the string
1422      */
1423     charIterator: function() {
1424         /**
1425          * @constructor
1426          */
1427         function _chiterator (istring) {
1428             this.index = 0;
1429             this.hasNext = function () {
1430                 return (this.index < istring.str.length);
1431             };
1432             this.next = function () {
1433                 var ch;
1434                 if (this.index < istring.str.length) {
1435                     ch = istring.str.charAt(this.index);
1436                     if (IString._isSurrogate(ch) &&
1437                             this.index+1 < istring.str.length &&
1438                             IString._isSurrogate(istring.str.charAt(this.index+1))) {
1439                         this.index++;
1440                         ch += istring.str.charAt(this.index);
1441                     }
1442                     this.index++;
1443                 }
1444                 return ch;
1445             };
1446         };
1447         return new _chiterator(this);
1448     },
1449 
1450     /**
1451      * Return the code point at the given index when the string is viewed
1452      * as an array of code points. If the index is beyond the end of the
1453      * array of code points or if the index is negative, -1 is returned.
1454      * @param {number} index index of the code point
1455      * @return {number} code point of the character at the given index into
1456      * the string
1457      */
1458     codePointAt: function (index) {
1459         if (index < 0) {
1460             return -1;
1461         }
1462         var count,
1463             it = this.iterator(),
1464             ch;
1465         for (count = index; count >= 0 && it.hasNext(); count--) {
1466             ch = it.next();
1467         }
1468         return (count < 0) ? ch : -1;
1469     },
1470 
1471     /**
1472      * Set the locale to use when processing choice formats. The locale
1473      * affects how number classes are interpretted. In some cultures,
1474      * the limit "few" maps to "any integer that ends in the digits 2 to 9" and
1475      * in yet others, "few" maps to "any integer that ends in the digits
1476      * 3 or 4".
1477      * @param {Locale|string} locale locale to use when processing choice
1478      * formats with this string
1479      * @param {boolean=} sync [optional] whether to load the locale data synchronously
1480      * or not
1481      * @param {Object=} loadParams [optional] parameters to pass to the loader function
1482      * @param {function(*)=} onLoad [optional] function to call when the loading is done
1483      */
1484     setLocale: function (locale, sync, loadParams, onLoad) {
1485         if (typeof(locale) === 'object') {
1486             this.locale = locale;
1487         } else {
1488             this.localeSpec = locale;
1489             this.locale = new Locale(locale);
1490         }
1491 
1492         if (this._isIntlPluralAvailable(this.locale)){
1493             this.intlPlural = new Intl.PluralRules(this.locale.getSpec());
1494         }
1495 
1496         IString.loadPlurals(typeof(sync) !== 'undefined' ? sync : true, this.locale, loadParams, onLoad);
1497     },
1498 
1499     /**
1500      * Return the locale to use when processing choice formats. The locale
1501      * affects how number classes are interpretted. In some cultures,
1502      * the limit "few" maps to "any integer that ends in the digits 2 to 9" and
1503      * in yet others, "few" maps to "any integer that ends in the digits
1504      * 3 or 4".
1505      * @return {string} localespec to use when processing choice
1506      * formats with this string
1507      */
1508     getLocale: function () {
1509         return (this.locale ? this.locale.getSpec() : this.localeSpec) || ilib.getLocale();
1510     },
1511 
1512     /**
1513      * Return the number of code points in this string. This may be different
1514      * than the number of characters, as the UTF-16 encoding that Javascript
1515      * uses for its basis returns surrogate pairs separately. Two 2-byte
1516      * surrogate characters together make up one character/code point in
1517      * the supplementary character planes. If your string contains no
1518      * characters in the supplementary planes, this method will return the
1519      * same thing as the length() method.
1520      * @return {number} the number of code points in this string
1521      */
1522     codePointLength: function () {
1523         if (this.cpLength === -1) {
1524             var it = this.iterator();
1525             this.cpLength = 0;
1526             while (it.hasNext()) {
1527                 this.cpLength++;
1528                 it.next();
1529             };
1530         }
1531         return this.cpLength;
1532     }
1533 };
1534 
1535 module.exports = IString;
1536