001: package net.sf.saxon.functions;
002:
003: import net.sf.saxon.Controller;
004: import net.sf.saxon.expr.Expression;
005: import net.sf.saxon.expr.StaticContext;
006: import net.sf.saxon.expr.Token;
007: import net.sf.saxon.expr.XPathContext;
008: import net.sf.saxon.om.*;
009: import net.sf.saxon.style.ExpressionContext;
010: import net.sf.saxon.tinytree.CharSlice;
011: import net.sf.saxon.trans.*;
012: import net.sf.saxon.value.*;
013:
014: import java.io.Serializable;
015: import java.math.BigDecimal;
016: import java.util.ArrayList;
017: import java.util.List;
018:
019: /**
020: * XSLT 2.0 implementation of format-number() function - removes the dependence on the JDK.
021: */
022:
023: public class FormatNumber2 extends SystemFunction implements
024: XSLTFunction {
025:
026: private NamespaceResolver nsContext = null;
027: // held only if the third argument is present, and its value is not known statically
028:
029: private DecimalSymbols decimalFormatSymbols = null;
030: // held only if the decimal format to use can be determined statically
031:
032: private transient String picture = null;
033: // held transiently at compile time if the picture is known statically
034:
035: private SubPicture[] subPictures = null;
036: // held if the picture is known statically
037:
038: private boolean requireFixup = false;
039: // used to detect when an unknown decimal-format name is used
040:
041: private transient boolean checked = false;
042:
043: // the second time checkArguments is called, it's a global check so the static context is inaccurate
044:
045: public void checkArguments(StaticContext env) throws XPathException {
046: if (checked)
047: return;
048: checked = true;
049: super .checkArguments(env);
050: if (argument[1] instanceof StringValue) {
051: // picture is known statically - optimize for this common case
052: picture = ((StringValue) argument[1]).getStringValue();
053: }
054: if (argument.length == 3) {
055: if (argument[2] instanceof StringValue) {
056: // common case, decimal format name is supplied as a string literal
057:
058: String qname = ((StringValue) argument[2])
059: .getStringValue();
060: String dfLocalName;
061: String dfURI;
062: try {
063: String[] parts = env.getConfiguration()
064: .getNameChecker().getQNameParts(qname);
065: dfLocalName = parts[1];
066: dfURI = env.getURIForPrefix(parts[0]);
067: } catch (QNameException e) {
068: throw new StaticError(
069: "Invalid decimal format name. "
070: + e.getMessage());
071: }
072:
073: DecimalFormatManager dfm = ((ExpressionContext) env)
074: .getXSLStylesheet().getDecimalFormatManager();
075: requireFixup = true;
076: dfm.registerUsage(dfURI, dfLocalName, this );
077: // this causes a callback to the fixup() method, either now, or later if it's a forwards reference
078: } else {
079: // we need to save the namespace context
080: nsContext = env.getNamespaceResolver();
081: }
082: } else {
083: // two arguments only: it uses the default decimal format
084: if (env instanceof ExpressionContext) {
085: // this is XSLT
086: DecimalFormatManager dfm = ((ExpressionContext) env)
087: .getXSLStylesheet().getDecimalFormatManager();
088: dfm.registerUsage("", "", this );
089: // Note: if using the "default default", there will be no fixup call.
090: } else {
091: // using saxon:decimal-format in some other environment
092: }
093: }
094: }
095:
096: /**
097: * Fixup: this is a callback from the DecimalFormatManager used once the xsl:decimal-format
098: * element is identified
099: */
100:
101: public void fixup(DecimalSymbols dfs) {
102: // System.err.println("Fixed up format-number, picture=" + picture);
103: requireFixup = false;
104: decimalFormatSymbols = dfs;
105: if (picture != null) {
106: try {
107: subPictures = getSubPictures(picture, dfs);
108: } catch (XPathException err) {
109: subPictures = null;
110: // we'll report the error at run-time
111: }
112: }
113: }
114:
115: /**
116: * Analyze a picture string into two sub-pictures.
117: * @return an array of two sub-pictures, the positive and the negative sub-pictures respectively.
118: * If there is only one sub-picture, the second one is null.
119: */
120:
121: private SubPicture[] getSubPictures(String picture,
122: DecimalSymbols dfs) throws XPathException {
123: int[] picture4 = StringValue.expand(picture);
124: SubPicture[] pics = new SubPicture[2];
125: if (picture4.length == 0) {
126: DynamicError err = new DynamicError(
127: "format-number() picture is zero-length");
128: err.setErrorCode("XTDE1310");
129: throw err;
130: }
131: int sep = -1;
132: for (int c = 0; c < picture4.length; c++) {
133: if (picture4[c] == dfs.patternSeparator) {
134: if (c == 0) {
135: grumble("first subpicture is zero-length");
136: } else if (sep >= 0) {
137: grumble("more than one pattern separator");
138: } else if (sep == picture4.length - 1) {
139: grumble("second subpicture is zero-length");
140: }
141: sep = c;
142: }
143: }
144:
145: if (sep < 0) {
146: pics[0] = new SubPicture(picture4, dfs);
147: pics[1] = null;
148: } else {
149: int[] pic0 = new int[sep];
150: System.arraycopy(picture4, 0, pic0, 0, sep);
151: int[] pic1 = new int[picture4.length - sep - 1];
152: System.arraycopy(picture4, sep + 1, pic1, 0,
153: picture4.length - sep - 1);
154: pics[0] = new SubPicture(pic0, dfs);
155: pics[1] = new SubPicture(pic1, dfs);
156: }
157: return pics;
158: }
159:
160: /**
161: * preEvaluate: this method suppresses compile-time evaluation by doing nothing.
162: * We can't evaluate early because we don't have access to the DecimalFormatManager.
163: */
164:
165: public Expression preEvaluate(StaticContext env)
166: throws XPathException {
167: return this ;
168: }
169:
170: /**
171: * Evaluate in a context where a string is wanted
172: */
173:
174: public String evaluateAsString(XPathContext context)
175: throws XPathException {
176:
177: int numArgs = argument.length;
178: Controller ctrl = context.getController();
179:
180: DecimalSymbols dfs = decimalFormatSymbols;
181:
182: AtomicValue av0 = (AtomicValue) argument[0]
183: .evaluateItem(context);
184: if (av0 == null) {
185: av0 = DoubleValue.NaN;
186: }
187: ;
188: NumericValue number = (NumericValue) av0.getPrimitiveValue();
189:
190: if (dfs == null) {
191: // the decimal-format name was not resolved statically
192: if (requireFixup) {
193: // we registered for a fixup, but none came
194: dynamicError("Unknown decimal format name", "XTDE1280",
195: context);
196: return null;
197: }
198: DecimalFormatManager dfm = ctrl.getExecutable()
199: .getDecimalFormatManager();
200: if (numArgs == 2) {
201: dfs = dfm.getDefaultDecimalFormat();
202: } else {
203: // the decimal-format name was given as a run-time expression
204: String qname = argument[2].evaluateItem(context)
205: .getStringValue();
206: try {
207: String[] parts = ctrl.getConfiguration()
208: .getNameChecker().getQNameParts(qname);
209: String localName = parts[1];
210: String uri = nsContext.getURIForPrefix(parts[0],
211: false);
212: if (uri == null) {
213: dynamicError("Namespace prefix '" + parts[0]
214: + "' has not been defined", "XTDE1280",
215: context);
216: return null;
217: }
218: dfs = dfm.getNamedDecimalFormat(uri, localName);
219: if (dfs == null) {
220: dynamicError(
221: "format-number function: decimal-format '"
222: + localName
223: + "' is not defined",
224: "XTDE1280", context);
225: return null;
226: }
227: } catch (QNameException e) {
228: dynamicError("Invalid decimal format name. "
229: + e.getMessage(), "XTDE1280", context);
230: }
231: }
232: }
233: SubPicture[] pics = subPictures;
234: if (pics == null) {
235: String format = argument[1].evaluateItem(context)
236: .getStringValue();
237: pics = getSubPictures(format, dfs);
238: }
239: return formatNumber(number, pics, dfs).toString();
240: }
241:
242: /**
243: * Evaluate in a general context
244: */
245:
246: public Item evaluateItem(XPathContext c) throws XPathException {
247: return new StringValue(evaluateAsString(c));
248: }
249:
250: /**
251: * Format a number, given the two subpictures and the decimal format symbols
252: */
253:
254: private CharSequence formatNumber(NumericValue number,
255: SubPicture[] subPictures, DecimalSymbols dfs) {
256:
257: NumericValue absN = number;
258: SubPicture pic;
259: String minusSign = "";
260: if (number.signum() < 0) {
261: absN = number.negate();
262: if (subPictures[1] == null) {
263: pic = subPictures[0];
264: minusSign = "" + unicodeChar(dfs.minusSign);
265: } else {
266: pic = subPictures[1];
267: }
268: } else {
269: pic = subPictures[0];
270: }
271:
272: return pic.format(absN, dfs, minusSign);
273: }
274:
275: private void grumble(String s) throws XPathException {
276: dynamicError("format-number picture: " + s, "XTDE1310", null);
277: }
278:
279: /**
280: * Convert a double to a BigDecimal. In general there will be several BigDecimal values that
281: * are equal to the supplied value, and the one we want to choose is the one with fewest non-zero
282: * digits. The algorithm used is rather pragmatic: look for a string of zeroes or nines, try rounding
283: * the number down or up as approriate, then convert the adjusted value to a double to see if it's
284: * equal to the original: if not, use the original value unchanged.
285: * @param value the double to be converted
286: * @param precision 2 for a double, 1 for a float
287: * @return the result of conversion to a double
288: */
289:
290: public static BigDecimal adjustToDecimal(double value, int precision) {
291: final String zeros = (precision == 1 ? "00000" : "000000000");
292: final String nines = (precision == 1 ? "99999" : "999999999");
293: BigDecimal initial = new BigDecimal(value);
294: BigDecimal trial = null;
295: String s = DecimalValue.decimalToString(initial);
296: int start = (s.charAt(0) == '-' ? 1 : 0);
297: int p = s.indexOf(".");
298: int i = s.lastIndexOf(zeros);
299: if (i > 0) {
300: if (p < 0 || i < p) {
301: // we're in the integer part
302: // try replacing all following digits with zeros and seeing if we get the same double back
303: FastStringBuffer sb = new FastStringBuffer(s.length());
304: sb.append(s.substring(0, i));
305: for (int n = i; n < s.length(); n++) {
306: sb.append(s.charAt(n) == '.' ? '.' : '0');
307: }
308: trial = new BigDecimal(sb.toString());
309: } else {
310: // we're in the fractional part
311: // try truncating the number before the zeros and seeing if we get the same double back
312: trial = new BigDecimal(s.substring(0, i));
313:
314: }
315: } else {
316: i = s.indexOf(nines);
317: if (i >= 0) {
318: if (i == start) {
319: // number starts with 99999... or -99999. Try rounding up to 100000.. or -100000...
320: FastStringBuffer sb = new FastStringBuffer(s
321: .length() + 1);
322: if (start == 1) {
323: sb.append('-');
324: }
325: sb.append('1');
326: for (int n = start; n < s.length(); n++) {
327: sb.append(s.charAt(n) == '.' ? '.' : '0');
328: }
329: trial = new BigDecimal(sb.toString());
330: } else {
331: // try rounding up
332: while (i >= 0
333: && (s.charAt(i) == '9' || s.charAt(i) == '.')) {
334: i--;
335: }
336: if (i < 0 || s.charAt(i) == '-') {
337: return initial; // can't happen: we've already handled numbers starting 99999..
338: } else if (p < 0 || i < p) {
339: // we're in the integer part
340: FastStringBuffer sb = new FastStringBuffer(s
341: .length());
342: sb.append(s.substring(0, i));
343: sb.append((char) ((int) s.charAt(i) + 1));
344: for (int n = i; n < s.length(); n++) {
345: sb.append(s.charAt(n) == '.' ? '.' : '0');
346: }
347: trial = new BigDecimal(sb.toString());
348: } else {
349: // we're in the fractional part - can ignore following digits
350: String s2 = s.substring(0, i)
351: + (char) ((int) s.charAt(i) + 1);
352: trial = new BigDecimal(s2);
353: }
354: }
355: }
356: }
357: if (trial != null
358: && (precision == 1 ? trial.floatValue() == value
359: : trial.doubleValue() == value)) {
360: return trial;
361: } else {
362: return initial;
363: }
364: }
365:
366: /**
367: * Inner class to represent one sub-picture (the negative or positive subpicture)
368: */
369:
370: private class SubPicture implements Serializable {
371:
372: int minWholePartSize = 0;
373: int maxWholePartSize = 0;
374: int minFractionPartSize = 0;
375: int maxFractionPartSize = 0;
376: boolean isPercent = false;
377: boolean isPerMille = false;
378: String prefix = "";
379: String suffix = "";
380: int[] wholePartGroupingPositions = null;
381: int[] fractionalPartGroupingPositions = null;
382:
383: public SubPicture(int[] pic, DecimalSymbols dfs)
384: throws XPathException {
385:
386: final int percentSign = dfs.percent;
387: final int perMilleSign = dfs.permill;
388: final int decimalSeparator = dfs.decimalSeparator;
389: final int groupingSeparator = dfs.groupingSeparator;
390: final int digitSign = dfs.digit;
391: final int zeroDigit = dfs.zeroDigit;
392:
393: List wholePartPositions = null;
394: List fractionalPartPositions = null;
395:
396: boolean foundDigit = false;
397: boolean foundDecimalSeparator = false;
398: for (int i = 0; i < pic.length; i++) {
399: if (pic[i] == digitSign || pic[i] == zeroDigit) {
400: foundDigit = true;
401: break;
402: }
403: }
404: if (!foundDigit) {
405: grumble("subpicture contains no digit or zero-digit sign");
406: }
407:
408: int phase = 0;
409: // phase = 0: passive characters at start
410: // phase = 1: digit signs in whole part
411: // phase = 2: zero-digit signs in whole part
412: // phase = 3: zero-digit signs in fractional part
413: // phase = 4: digit signs in fractional part
414: // phase = 5: passive characters at end
415:
416: for (int i = 0; i < pic.length; i++) {
417: int c = pic[i];
418:
419: if (c == percentSign || c == perMilleSign) {
420: if (isPercent || isPerMille) {
421: grumble("Cannot have more than one percent or per-mille character in a sub-picture");
422: }
423: isPercent = (c == percentSign);
424: isPerMille = (c == perMilleSign);
425: switch (phase) {
426: case 0:
427: prefix += unicodeChar(c);
428: break;
429: case 1:
430: case 2:
431: case 3:
432: case 4:
433: case 5:
434: phase = 5;
435: suffix += unicodeChar(c);
436: break;
437: }
438: } else if (c == digitSign) {
439: switch (phase) {
440: case 0:
441: case 1:
442: phase = 1;
443: maxWholePartSize++;
444: break;
445: case 2:
446: grumble("Digit sign must not appear after a zero-digit sign in the integer part of a sub-picture");
447: break;
448: case 3:
449: case 4:
450: phase = 4;
451: maxFractionPartSize++;
452: break;
453: case 5:
454: grumble("Passive character must not appear between active characters in a sub-picture");
455: break;
456: }
457: } else if (c == zeroDigit) {
458: switch (phase) {
459: case 0:
460: case 1:
461: case 2:
462: phase = 2;
463: minWholePartSize++;
464: maxWholePartSize++;
465: break;
466: case 3:
467: minFractionPartSize++;
468: maxFractionPartSize++;
469: break;
470: case 4:
471: grumble("Zero digit sign must not appear after a digit sign in the fractional part of a sub-picture");
472: break;
473: case 5:
474: grumble("Passive character must not appear between active characters in a sub-picture");
475: break;
476: }
477: } else if (c == decimalSeparator) {
478: switch (phase) {
479: case 0:
480: case 1:
481: case 2:
482: phase = 3;
483: foundDecimalSeparator = true;
484: break;
485: case 3:
486: case 4:
487: case 5:
488: if (foundDecimalSeparator) {
489: grumble("There must only be one decimal separator in a sub-picture");
490: } else {
491: grumble("Decimal separator cannot come after a character in the suffix");
492: }
493: break;
494: }
495: } else if (c == groupingSeparator) {
496: switch (phase) {
497: case 0:
498: case 1:
499: case 2:
500: if (wholePartPositions == null) {
501: wholePartPositions = new ArrayList(3);
502: }
503: wholePartPositions.add(new Integer(
504: maxWholePartSize));
505: // note these are positions from a false offset, they will be corrected later
506: break;
507: case 3:
508: case 4:
509: if (maxFractionPartSize == 0) {
510: grumble("Grouping separator cannot be adjacent to decimal separator");
511: }
512: if (fractionalPartPositions == null) {
513: fractionalPartPositions = new ArrayList(3);
514: }
515: fractionalPartPositions.add(new Integer(
516: maxFractionPartSize));
517: break;
518: case 5:
519: grumble("Grouping separator found in suffix of sub-picture");
520: break;
521: }
522: } else { // passive character found
523: switch (phase) {
524: case 0:
525: prefix += unicodeChar(c);
526: break;
527: case 1:
528: case 2:
529: case 3:
530: case 4:
531: case 5:
532: phase = 5;
533: suffix += unicodeChar(c);
534: break;
535: }
536: }
537: }
538:
539: // System.err.println("minWholePartSize = " + minWholePartSize);
540: // System.err.println("maxWholePartSize = " + maxWholePartSize);
541: // System.err.println("minFractionPartSize = " + minFractionPartSize);
542: // System.err.println("maxFractionPartSize = " + maxFractionPartSize);
543:
544: // Sort out the grouping positions
545:
546: if (wholePartPositions != null) {
547: // convert to positions relative to the decimal separator
548: int n = wholePartPositions.size();
549: wholePartGroupingPositions = new int[n];
550: for (int i = 0; i < n; i++) {
551: wholePartGroupingPositions[i] = maxWholePartSize
552: - ((Integer) wholePartPositions.get(n - i
553: - 1)).intValue();
554: }
555: if (n > 1) {
556: boolean regular = true;
557: int first = wholePartGroupingPositions[0];
558: for (int i = 1; i < n; i++) {
559: if (wholePartGroupingPositions[i] != i * first) {
560: regular = false;
561: break;
562: }
563: }
564: if (regular) {
565: wholePartGroupingPositions = new int[1];
566: wholePartGroupingPositions[0] = first;
567: }
568: }
569: if (wholePartGroupingPositions[0] == 0) {
570: grumble("Cannot have a grouping separator adjacent to the decimal separator");
571: }
572: }
573:
574: if (fractionalPartPositions != null) {
575: int n = fractionalPartPositions.size();
576: fractionalPartGroupingPositions = new int[n];
577: for (int i = 0; i < n; i++) {
578: fractionalPartGroupingPositions[i] = ((Integer) fractionalPartPositions
579: .get(i)).intValue();
580: }
581: }
582: }
583:
584: /**
585: * Format a number using this sub-picture
586: * @param value the absolute value of the number to be formatted
587: */
588:
589: public CharSequence format(NumericValue value,
590: DecimalSymbols dfs, String minusSign) {
591:
592: // System.err.println("Formatting " + value);
593:
594: if (value.isNaN()) {
595: return prefix + dfs.NaN + suffix;
596: }
597:
598: if (value instanceof DoubleValue
599: && Double.isInfinite(value.getDoubleValue())) {
600: return minusSign + prefix + dfs.infinity + suffix;
601: }
602:
603: if (value instanceof FloatValue
604: && Double.isInfinite(value.getDoubleValue())) {
605: return minusSign + prefix + dfs.infinity + suffix;
606: }
607:
608: int multiplier = 1;
609: if (isPercent) {
610: multiplier = 100;
611: } else if (isPerMille) {
612: multiplier = 1000;
613: }
614:
615: if (multiplier != 1) {
616: try {
617: value = value.arithmetic(Token.MULT,
618: new IntegerValue(multiplier), null);
619: } catch (XPathException e) {
620: value = new DoubleValue(value.getDoubleValue()
621: * multiplier);
622: }
623: }
624:
625: StringBuffer sb = new StringBuffer(20);
626: if (value instanceof DoubleValue
627: || value instanceof FloatValue) {
628: BigDecimal dec = adjustToDecimal(
629: value.getDoubleValue(), 2);
630: formatDecimal(dec, sb);
631:
632: //formatDouble(value.getDoubleValue(), sb);
633:
634: } else if (value instanceof IntegerValue
635: || value instanceof BigIntegerValue) {
636: formatInteger(value, sb);
637:
638: } else if (value instanceof DecimalValue) {
639: formatDecimal(((DecimalValue) value).getValue(), sb);
640: }
641:
642: // System.err.println("Justified number: " + sb.toString());
643:
644: // Map the digits and decimal point to use the selected characters
645:
646: int[] ib = StringValue.expand(sb);
647: int ibused = ib.length;
648: int point = sb.indexOf(".");
649: if (point == -1) {
650: point = sb.length();
651: } else {
652: ib[point] = dfs.decimalSeparator;
653:
654: // If there is no fractional part, delete the decimal point
655: if (maxFractionPartSize == 0) {
656: ibused--;
657: }
658: }
659:
660: // Map the digits
661:
662: if (dfs.zeroDigit != '0') {
663: int newZero = dfs.zeroDigit;
664: for (int i = 0; i < ibused; i++) {
665: int c = ib[i];
666: if (c >= '0' && c <= '9') {
667: ib[i] = (c - '0' + newZero);
668: }
669: }
670: }
671:
672: // Add the whole-part grouping separators
673:
674: if (wholePartGroupingPositions != null) {
675: if (wholePartGroupingPositions.length == 1) {
676: // grouping separators are at regular positions
677: int g = wholePartGroupingPositions[0];
678: int p = point - g;
679: while (p > 0) {
680: ib = insert(ib, ibused++,
681: dfs.groupingSeparator, p);
682: //sb.insert(p, unicodeChar(dfs.groupingSeparator));
683: p -= g;
684: }
685: } else {
686: // grouping separators are at irregular positions
687: for (int i = 0; i < wholePartGroupingPositions.length; i++) {
688: int p = point - wholePartGroupingPositions[i];
689: if (p > 0) {
690: ib = insert(ib, ibused++,
691: dfs.groupingSeparator, p);
692: //sb.insert(p, unicodeChar(dfs.groupingSeparator));
693: }
694: }
695: }
696: }
697:
698: // Add the fractional-part grouping separators
699:
700: if (fractionalPartGroupingPositions != null) {
701: // grouping separators are at irregular positions.
702: for (int i = 0; i < fractionalPartGroupingPositions.length; i++) {
703: int p = point + 1
704: + fractionalPartGroupingPositions[i] + i;
705: if (p < ibused - 1) {
706: ib = insert(ib, ibused++,
707: dfs.groupingSeparator, p);
708: //sb.insert(p, dfs.groupingSeparator);
709: } else {
710: break;
711: }
712: }
713: }
714:
715: // System.err.println("Grouped number: " + sb.toString());
716:
717: //sb.insert(0, prefix);
718: //sb.insert(0, minusSign);
719: //sb.append(suffix);
720: FastStringBuffer res = new FastStringBuffer(prefix.length()
721: + minusSign.length() + suffix.length() + ibused);
722: res.append(minusSign);
723: res.append(prefix);
724: res.append(StringValue.contract(ib, ibused));
725: res.append(suffix);
726: return res;
727: }
728:
729: /**
730: * Format a number supplied as a decimal
731: * @param dval the decimal value
732: * @param sb the stringBuffer to contain the result
733: */
734: private void formatDecimal(BigDecimal dval, StringBuffer sb) {
735: dval = dval.setScale(maxFractionPartSize,
736: BigDecimal.ROUND_HALF_EVEN);
737: sb.append(dval.toString());
738:
739: int point = sb.indexOf(".");
740: int intDigits;
741: if (point >= 0) {
742: int zz = maxFractionPartSize - minFractionPartSize;
743: while (zz > 0) {
744: if (sb.charAt(sb.length() - 1) == '0') {
745: sb.setLength(sb.length() - 1);
746: zz--;
747: } else {
748: break;
749: }
750: }
751: intDigits = point;
752: if (sb.charAt(sb.length() - 1) == '.') {
753: sb.setLength(sb.length() - 1);
754: }
755: } else {
756: intDigits = sb.length();
757: }
758: for (int i = 0; i < (minWholePartSize - intDigits); i++) {
759: sb.insert(0, '0');
760: }
761: }
762:
763: /**
764: * Format a number supplied as a integer
765: * @param value the integer value
766: * @param sb the stringBuffer to contain the result
767: */
768:
769: private void formatInteger(NumericValue value, StringBuffer sb) {
770: sb.append(value.toString());
771: int leadingZeroes = minWholePartSize - sb.length();
772: for (int i = 0; i < leadingZeroes; i++) {
773: sb.insert(0, '0');
774: }
775: if (minFractionPartSize != 0) {
776: sb.append('.');
777: for (int i = 0; i < minFractionPartSize; i++) {
778: sb.append('0');
779: }
780: }
781: }
782: }
783:
784: /**
785: * Convert a Unicode character (possibly >65536) to a String, using a surrogate pair if necessary
786: * @param ch the Unicode codepoint value
787: * @return a string representing the Unicode codepoint, either a string of one character or a surrogate pair
788: */
789:
790: private static CharSequence unicodeChar(int ch) {
791: if (ch < 65536) {
792: return "" + (char) ch;
793: } else { // output a surrogate pair
794: //To compute the numeric value of the character corresponding to a surrogate
795: //pair, use this formula (all numbers are hex):
796: //(FirstChar - D800) * 400 + (SecondChar - DC00) + 10000
797: ch -= 65536;
798: char[] sb = new char[2];
799: sb[0] = ((char) ((ch / 1024) + 55296));
800: sb[1] = ((char) ((ch % 1024) + 56320));
801: return new CharSlice(sb, 0, 2);
802: }
803: }
804:
805: /**
806: * Insert an integer into an array of integers
807: */
808:
809: private static int[] insert(int[] array, int used, int value,
810: int position) {
811: if (used + 1 > array.length) {
812: int[] a2 = new int[used + 10];
813: System.arraycopy(array, 0, a2, 0, used);
814: array = a2;
815: }
816: for (int i = used - 1; i >= position; i--) {
817: array[i + 1] = array[i];
818: }
819: array[position] = value;
820: return array;
821: }
822: }
823:
824: //
825: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
826: // you may not use this file except in compliance with the License. You may obtain a copy of the
827: // License at http://www.mozilla.org/MPL/
828: //
829: // Software distributed under the License is distributed on an "AS IS" basis,
830: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
831: // See the License for the specific language governing rights and limitations under the License.
832: //
833: // The Original Code is: all this file.
834: //
835: // The Initial Developer of the Original Code is Michael H. Kay
836: //
837: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
838: //
839: // Contributor(s): none.
840: //
|