You are viewing a plain text version of this content. The canonical link for it is here.
Posted to fop-commits@xmlgraphics.apache.org by ga...@apache.org on 2012/02/26 03:29:29 UTC
svn commit: r1293736 [15/38] - in /xmlgraphics/fop/trunk: ./
src/codegen/java/org/apache/fop/tools/
src/codegen/unicode/java/org/apache/fop/complexscripts/
src/codegen/unicode/java/org/apache/fop/complexscripts/bidi/
src/documentation/content/xdocs/tru...
Added: xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/NumberConverter.java
URL: http://svn.apache.org/viewvc/xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/NumberConverter.java?rev=1293736&view=auto
==============================================================================
--- xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/NumberConverter.java (added)
+++ xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/NumberConverter.java Sun Feb 26 02:29:01 2012
@@ -0,0 +1,1616 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.complexscripts.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// CSOFF: LineLengthCheck
+// CSOFF: InnerAssignmentCheck
+// CSOFF: NoWhitespaceAfterCheck
+// CSOFF: AvoidNestedBlocksCheck
+
+/**
+ * Implementation of Number to String Conversion algorithm specified by
+ * XSL Transformations (XSLT) Version 2.0, W3C Recommendation, 23 January 2007.
+ *
+ * This algorithm differs from that specified in XSLT 1.0 in the following
+ * ways:
+ * <ul>
+ * <li>input numbers are greater than or equal to zero rather than greater than zero;</li>
+ * <li>introduces format tokens { w, W, Ww };</li>
+ * <li>introduces ordinal parameter to generate ordinal numbers;</li>
+ * </ul>
+ *
+ * Implementation Defaults and Limitations
+ * <ul>
+ * <li>If language parameter is unspecified (null or empty string), then the value
+ * of DEFAULT_LANGUAGE is used, which is defined below as "eng" (English).</li>
+ * <li>Only English, French, and Spanish word numerals are supported, and only if less than one trillion (1,000,000,000,000).</li>
+ * <li>Ordinal word numerals are supported for French and Spanish only when less than or equal to ten (10).</li>
+ * </ul>
+ *
+ * Implementation Notes
+ * <ul>
+ * <li>In order to handle format tokens outside the Unicode BMP, all processing is
+ * done in Unicode Scalar Values represented with Integer and Integer[]
+ * types. Without affecting behavior, this may be subsequently optimized to
+ * use int and int[] types.</li>
+ * <li>In order to communicate various sub-parameters, including ordinalization, a <em>features</em>
+ * is employed, which consists of comma separated name and optional value tokens, where name and value
+ * are separated by an equals '=' sign.</li>
+ * <li>Ordinal numbers are selected by specifying a word based format token in combination with a 'ordinal' feature with no value, in which case
+ * the features 'male' and 'female' may be used to specify gender for gender sensitive languages. For example, the feature string "ordinal,female"
+ * selects female ordinals.</li>
+ * </ul>
+ *
+ * @author Glenn Adams
+ */
+public class NumberConverter {
+
+ /** alphabetical */
+ public static final int LETTER_VALUE_ALPHABETIC = 1;
+ /** traditional */
+ public static final int LETTER_VALUE_TRADITIONAL = 2;
+
+ /** no token type */
+ private static final int TOKEN_NONE = 0;
+ /** alhphanumeric token type */
+ private static final int TOKEN_ALPHANUMERIC = 1;
+ /** nonalphanumeric token type */
+ private static final int TOKEN_NONALPHANUMERIC = 2;
+ /** default token */
+ private static final Integer[] DEFAULT_TOKEN = new Integer[] { (int) '1' };
+ /** default separator */
+ private static final Integer[] DEFAULT_SEPARATOR = new Integer[] { (int) '.' };
+ /** default language */
+ private static final String DEFAULT_LANGUAGE = "eng";
+
+ /** prefix token */
+ private Integer[] prefix;
+ /** suffix token */
+ private Integer[] suffix;
+ /** sequence of tokens, as parsed from format */
+ private Integer[][] tokens;
+ /** sequence of separators, as parsed from format */
+ private Integer[][] separators;
+ /** grouping separator */
+ private int groupingSeparator;
+ /** grouping size */
+ private int groupingSize;
+ /** letter value */
+ private int letterValue;
+ /** letter value system */
+ private String features;
+ /** language */
+ private String language;
+ /** country */
+ private String country;
+
+ /**
+ * Construct parameterized number converter.
+ * @param format format for the page number (may be null or empty, which is treated as null)
+ * @param groupingSeparator grouping separator (if zero, then no grouping separator applies)
+ * @param groupingSize grouping size (if zero or negative, then no grouping size applies)
+ * @param letterValue letter value (must be one of the above letter value enumeration values)
+ * @param features features (feature sub-parameters)
+ * @param language (may be null or empty, which is treated as null)
+ * @param country (may be null or empty, which is treated as null)
+ * @throws IllegalArgumentException if format is not a valid UTF-16 string (e.g., has unpaired surrogate)
+ */
+ public NumberConverter ( String format, int groupingSeparator, int groupingSize, int letterValue, String features, String language, String country )
+ throws IllegalArgumentException {
+ this.groupingSeparator = groupingSeparator;
+ this.groupingSize = groupingSize;
+ this.letterValue = letterValue;
+ this.features = features;
+ this.language = ( language != null ) ? language.toLowerCase() : null;
+ this.country = ( country != null ) ? country.toLowerCase() : null;
+ parseFormatTokens ( format );
+ }
+
+ /**
+ * Convert a number to string according to conversion parameters.
+ * @param number number to conver
+ * @return string representing converted number
+ */
+ public String convert ( long number ) {
+ List<Long> numbers = new ArrayList<Long>();
+ numbers.add ( number );
+ return convert ( numbers );
+ }
+
+ /**
+ * Convert list of numbers to string according to conversion parameters.
+ * @param numbers list of numbers to convert
+ * @return string representing converted list of numbers
+ */
+ public String convert ( List<Long> numbers ) {
+ List<Integer> scalars = new ArrayList<Integer>();
+ if ( prefix != null ) {
+ appendScalars ( scalars, prefix );
+ }
+ convertNumbers ( scalars, numbers );
+ if ( suffix != null ) {
+ appendScalars ( scalars, suffix );
+ }
+ return scalarsToString ( scalars );
+ }
+
+ private void parseFormatTokens ( String format ) throws IllegalArgumentException {
+ List<Integer[]> tokens = new ArrayList<Integer[]>();
+ List<Integer[]> separators = new ArrayList<Integer[]>();
+ if ( ( format == null ) || ( format.length() == 0 ) ) {
+ format = "1";
+ }
+ int tokenType = TOKEN_NONE;
+ List<Integer> token = new ArrayList<Integer>();
+ Integer[] ca = UTF32.toUTF32 ( format, 0, true );
+ for ( int i = 0, n = ca.length; i < n; i++ ) {
+ int c = ca[i];
+ int tokenTypeNew = isAlphaNumeric ( c ) ? TOKEN_ALPHANUMERIC : TOKEN_NONALPHANUMERIC;
+ if ( tokenTypeNew != tokenType ) {
+ if ( token.size() > 0 ) {
+ if ( tokenType == TOKEN_ALPHANUMERIC ) {
+ tokens.add ( token.toArray ( new Integer [ token.size() ] ) );
+ } else {
+ separators.add ( token.toArray ( new Integer [ token.size() ] ) );
+ }
+ token.clear();
+ }
+ tokenType = tokenTypeNew;
+ }
+ token.add ( c );
+ }
+ if ( token.size() > 0 ) {
+ if ( tokenType == TOKEN_ALPHANUMERIC ) {
+ tokens.add ( token.toArray ( new Integer [ token.size() ] ) );
+ } else {
+ separators.add ( token.toArray ( new Integer [ token.size() ] ) );
+ }
+ }
+ if ( ! separators.isEmpty() ) {
+ this.prefix = separators.remove ( 0 );
+ }
+ if ( ! separators.isEmpty() ) {
+ this.suffix = separators.remove ( separators.size() - 1 );
+ }
+ this.separators = separators.toArray ( new Integer [ separators.size() ] [] );
+ this.tokens = tokens.toArray ( new Integer [ tokens.size() ] [] );
+ }
+
+ private static boolean isAlphaNumeric ( int c ) {
+ switch ( Character.getType ( c ) ) {
+ case Character.DECIMAL_DIGIT_NUMBER: // Nd
+ case Character.LETTER_NUMBER: // Nl
+ case Character.OTHER_NUMBER: // No
+ case Character.UPPERCASE_LETTER: // Lu
+ case Character.LOWERCASE_LETTER: // Ll
+ case Character.TITLECASE_LETTER: // Lt
+ case Character.MODIFIER_LETTER: // Lm
+ case Character.OTHER_LETTER: // Lo
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void convertNumbers ( List<Integer> scalars, List<Long> numbers ) {
+ Integer[] tknLast = DEFAULT_TOKEN;
+ int tknIndex = 0;
+ int tknCount = tokens.length;
+ int sepIndex = 0;
+ int sepCount = separators.length;
+ int numIndex = 0;
+ for ( Long number : numbers ) {
+ Integer[] sep = null;
+ Integer[] tkn;
+ if ( tknIndex < tknCount ) {
+ if ( numIndex > 0 ) {
+ if ( sepIndex < sepCount ) {
+ sep = separators [ sepIndex++ ];
+ } else {
+ sep = DEFAULT_SEPARATOR;
+ }
+ }
+ tkn = tokens [ tknIndex++ ];
+ } else {
+ tkn = tknLast;
+ }
+ appendScalars ( scalars, convertNumber ( number, sep, tkn ) );
+ tknLast = tkn;
+ numIndex++;
+ }
+ }
+
+ private Integer[] convertNumber ( long number, Integer[] separator, Integer[] token ) {
+ List<Integer> sl = new ArrayList<Integer>();
+ if ( separator != null ) {
+ appendScalars ( sl, separator );
+ }
+ if ( token != null ) {
+ appendScalars ( sl, formatNumber ( number, token ) );
+ }
+ return sl.toArray ( new Integer [ sl.size() ] );
+ }
+
+ private Integer[] formatNumber ( long number, Integer[] token ) {
+ Integer[] fn = null;
+ assert token.length > 0;
+ if ( number < 0 ) {
+ throw new IllegalArgumentException ( "number must be non-negative" );
+ } else if ( token.length == 1 ) {
+ int s = token[0].intValue();
+ switch ( s ) {
+ case (int) '1':
+ {
+ fn = formatNumberAsDecimal ( number, (int) '1', 1 );
+ break;
+ }
+ case (int) 'W':
+ case (int) 'w':
+ {
+ fn = formatNumberAsWord ( number, ( s == (int) 'W' ) ? Character.UPPERCASE_LETTER : Character.LOWERCASE_LETTER );
+ break;
+ }
+ case (int) 'A': // handled as numeric sequence
+ case (int) 'a': // handled as numeric sequence
+ case (int) 'I': // handled as numeric special
+ case (int) 'i': // handled as numeric special
+ default:
+ {
+ if ( isStartOfDecimalSequence ( s ) ) {
+ fn = formatNumberAsDecimal ( number, s, 1 );
+ } else if ( isStartOfAlphabeticSequence ( s ) ) {
+ fn = formatNumberAsSequence ( number, s, getSequenceBase ( s ), null );
+ } else if ( isStartOfNumericSpecial ( s ) ) {
+ fn = formatNumberAsSpecial ( number, s );
+ } else {
+ fn = null;
+ }
+ break;
+ }
+ }
+ } else if ( ( token.length == 2 ) && ( token[0] == (int) 'W' ) && ( token[1] == (int) 'w' ) ) {
+ fn = formatNumberAsWord ( number, Character.TITLECASE_LETTER );
+ } else if ( isPaddedOne ( token ) ) {
+ int s = token [ token.length - 1 ].intValue();
+ fn = formatNumberAsDecimal ( number, s, token.length );
+ } else {
+ throw new IllegalArgumentException ( "invalid format token: \"" + UTF32.fromUTF32 ( token ) + "\"" );
+ }
+ if ( fn == null ) {
+ fn = formatNumber ( number, DEFAULT_TOKEN );
+ }
+ assert fn != null;
+ return fn;
+ }
+
+ /**
+ * Format NUMBER as decimal using characters denoting digits that start at ONE,
+ * adding one or more (zero) padding characters as needed to fill out field WIDTH.
+ * @param number to be formatted
+ * @param one unicode scalar value denoting numeric value 1
+ * @param width non-negative integer denoting field width of number, possible including padding
+ * @return formatted number as array of unicode scalars
+ */
+ private Integer[] formatNumberAsDecimal ( long number, int one, int width ) {
+ assert Character.getNumericValue ( one ) == 1;
+ assert Character.getNumericValue ( one - 1 ) == 0;
+ assert Character.getNumericValue ( one + 8 ) == 9;
+ List<Integer> sl = new ArrayList<Integer>();
+ int zero = one - 1;
+ while ( number > 0 ) {
+ long digit = number % 10;
+ sl.add ( 0, zero + (int) digit );
+ number = number / 10;
+ }
+ while ( width > sl.size() ) {
+ sl.add ( 0, zero );
+ }
+ if ( ( groupingSize != 0 ) && ( groupingSeparator != 0 ) ) {
+ sl = performGrouping ( sl, groupingSize, groupingSeparator );
+ }
+ return sl.toArray ( new Integer [ sl.size() ] );
+ }
+
+ private static List<Integer> performGrouping ( List<Integer> sl, int groupingSize, int groupingSeparator ) {
+ assert groupingSize > 0;
+ assert groupingSeparator != 0;
+ if ( sl.size() > groupingSize ) {
+ List<Integer> gl = new ArrayList<Integer>();
+ for ( int i = 0, n = sl.size(), g = 0; i < n; i++ ) {
+ int k = n - i - 1;
+ if ( g == groupingSize ) {
+ gl.add ( 0, groupingSeparator );
+ g = 1;
+ } else {
+ g++;
+ }
+ gl.add ( 0, sl.get ( k ) );
+ }
+ return gl;
+ } else {
+ return sl;
+ }
+ }
+
+
+ /**
+ * Format NUMBER as using sequence of characters that start at ONE, and
+ * having BASE radix.
+ * @param number to be formatted
+ * @param one unicode scalar value denoting start of sequence (numeric value 1)
+ * @param base number of elements in sequence
+ * @param map if non-null, then maps sequences indices to unicode scalars
+ * @return formatted number as array of unicode scalars
+ */
+ private Integer[] formatNumberAsSequence ( long number, int one, int base, int[] map ) {
+ assert base > 1;
+ assert ( map == null ) || ( map.length >= base );
+ List<Integer> sl = new ArrayList<Integer>();
+ if ( number == 0 ) {
+ return null;
+ } else {
+ long n = number;
+ while ( n > 0 ) {
+ int d = (int) ( ( n - 1 ) % (long) base );
+ int s = ( map != null ) ? map [ d ] : ( one + d );
+ sl.add ( 0, s );
+ n = ( n - 1 ) / base;
+ }
+ return sl.toArray ( new Integer [ sl.size() ] );
+ }
+ }
+
+ /**
+ * Format NUMBER as using special system that starts at ONE.
+ * @param number to be formatted
+ * @param one unicode scalar value denoting start of system (numeric value 1)
+ * @return formatted number as array of unicode scalars
+ */
+ private Integer[] formatNumberAsSpecial ( long number, int one ) {
+ SpecialNumberFormatter f = getSpecialFormatter ( one, letterValue, features, language, country );
+ if ( f != null ) {
+ return f.format ( number, one, letterValue, features, language, country );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Format NUMBER as word according to TYPE, which must be either
+ * Character.UPPERCASE_LETTER, Character.LOWERCASE_LETTER, or
+ * Character.TITLECASE_LETTER. Makes use of this.language to
+ * determine language of word.
+ * @param number to be formatted
+ * @param caseType unicode character type for case conversion
+ * @return formatted number as array of unicode scalars
+ */
+ private Integer[] formatNumberAsWord ( long number, int caseType ) {
+ SpecialNumberFormatter f = null;
+ if ( isLanguage ( "eng" ) ) {
+ f = new EnglishNumberAsWordFormatter ( caseType );
+ } else if ( isLanguage ( "spa" ) ) {
+ f = new SpanishNumberAsWordFormatter ( caseType );
+ } else if ( isLanguage ( "fra" ) ) {
+ f = new FrenchNumberAsWordFormatter ( caseType );
+ } else {
+ f = new EnglishNumberAsWordFormatter ( caseType );
+ }
+ return f.format ( number, 0, letterValue, features, language, country );
+ }
+
+ private boolean isLanguage ( String iso3Code ) {
+ if ( language == null ) {
+ return false;
+ } else if ( language.equals ( iso3Code ) ) {
+ return true;
+ } else {
+ return isSameLanguage ( iso3Code, language );
+ }
+ }
+
+ private static String[][] equivalentLanguages = {
+ { "eng", "en" },
+ { "fra", "fre", "fr" },
+ { "spa", "es" },
+ };
+
+ private static boolean isSameLanguage ( String i3c, String lc ) {
+ for ( String[] el : equivalentLanguages ) {
+ assert el.length >= 2;
+ if ( el[0].equals ( i3c ) ) {
+ for ( int i = 0, n = el.length; i < n; i++ ) {
+ if ( el[i].equals ( lc ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasFeature ( String features, String feature ) {
+ if ( features != null ) {
+ assert feature != null;
+ assert feature.length() != 0;
+ String[] fa = features.split(",");
+ for ( String f : fa ) {
+ String[] fp = f.split("=");
+ assert fp.length > 0;
+ String fn = fp[0];
+ String fv = ( fp.length > 1 ) ? fp[1] : "";
+ if ( fn.equals ( feature ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /* not yet used
+ private static String getFeatureValue ( String features, String feature ) {
+ if ( features != null ) {
+ assert feature != null;
+ assert feature.length() != 0;
+ String[] fa = features.split(",");
+ for ( String f : fa ) {
+ String[] fp = f.split("=");
+ assert fp.length > 0;
+ String fn = fp[0];
+ String fv = ( fp.length > 1 ) ? fp[1] : "";
+ if ( fn.equals ( feature ) ) {
+ return fv;
+ }
+ }
+ }
+ return "";
+ }
+ */
+
+ private static void appendScalars ( List<Integer> scalars, Integer[] sa ) {
+ for ( Integer s : sa ) {
+ scalars.add ( s );
+ }
+ }
+
+ private static String scalarsToString ( List<Integer> scalars ) {
+ Integer[] sa = scalars.toArray ( new Integer [ scalars.size() ] );
+ return UTF32.fromUTF32 ( sa );
+ }
+
+ private static boolean isPaddedOne ( Integer[] token ) {
+ if ( getDecimalValue ( token [ token.length - 1 ] ) != 1 ) {
+ return false;
+ } else {
+ for ( int i = 0, n = token.length - 1; i < n; i++ ) {
+ if ( getDecimalValue ( token [ i ] ) != 0 ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private static int getDecimalValue ( Integer scalar ) {
+ int s = scalar.intValue();
+ if ( Character.getType ( s ) == Character.DECIMAL_DIGIT_NUMBER ) {
+ return Character.getNumericValue ( s );
+ } else {
+ return -1;
+ }
+ }
+
+ private static boolean isStartOfDecimalSequence ( int s ) {
+ return ( Character.getNumericValue ( s ) == 1 )
+ && ( Character.getNumericValue ( s - 1 ) == 0 )
+ && ( Character.getNumericValue ( s + 8 ) == 9 );
+ }
+
+ private static int[][] supportedAlphabeticSequences = {
+ { 'A', 26 }, // A...Z
+ { 'a', 26 }, // a...z
+ };
+
+ private static boolean isStartOfAlphabeticSequence ( int s ) {
+ for ( int[] ss : supportedAlphabeticSequences ) {
+ assert ss.length >= 2;
+ if ( ss[0] == s ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int getSequenceBase ( int s ) {
+ for ( int[] ss : supportedAlphabeticSequences ) {
+ assert ss.length >= 2;
+ if ( ss[0] == s ) {
+ return ss[1];
+ }
+ }
+ return 0;
+ }
+
+ private static int[][] supportedSpecials = {
+ { 'I' }, // latin - uppercase roman numerals
+ { 'i' }, // latin - lowercase roman numerals
+ { '\u0391' }, // greek - uppercase isopsephry numerals
+ { '\u03B1' }, // greek - lowercase isopsephry numerals
+ { '\u05D0' }, // hebrew - gematria numerals
+ { '\u0623' }, // arabic - abjadi numberals
+ { '\u0627' }, // arabic - either abjadi or hijai alphabetic sequence
+ { '\u0E01' }, // thai - default alphabetic sequence
+ { '\u3042' }, // kana - hiragana (gojuon) - default alphabetic sequence
+ { '\u3044' }, // kana - hiragana (iroha)
+ { '\u30A2' }, // kana - katakana (gojuon) - default alphabetic sequence
+ { '\u30A4' }, // kana - katakana (iroha)
+ };
+
+ private static boolean isStartOfNumericSpecial ( int s ) {
+ for ( int[] ss : supportedSpecials ) {
+ assert ss.length >= 1;
+ if ( ss[0] == s ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private SpecialNumberFormatter getSpecialFormatter ( int one, int letterValue, String features, String language, String country ) {
+ if ( one == (int) 'I' ) {
+ return new RomanNumeralsFormatter();
+ } else if ( one == (int) 'i' ) {
+ return new RomanNumeralsFormatter();
+ } else if ( one == (int) '\u0391' ) {
+ return new IsopsephryNumeralsFormatter();
+ } else if ( one == (int) '\u03B1' ) {
+ return new IsopsephryNumeralsFormatter();
+ } else if ( one == (int) '\u05D0' ) {
+ return new GematriaNumeralsFormatter();
+ } else if ( one == (int) '\u0623' ) {
+ return new ArabicNumeralsFormatter();
+ } else if ( one == (int) '\u0627' ) {
+ return new ArabicNumeralsFormatter();
+ } else if ( one == (int) '\u0E01' ) {
+ return new ThaiNumeralsFormatter();
+ } else if ( one == (int) '\u3042' ) {
+ return new KanaNumeralsFormatter();
+ } else if ( one == (int) '\u3044' ) {
+ return new KanaNumeralsFormatter();
+ } else if ( one == (int) '\u30A2' ) {
+ return new KanaNumeralsFormatter();
+ } else if ( one == (int) '\u30A4' ) {
+ return new KanaNumeralsFormatter();
+ } else {
+ return null;
+ }
+ }
+
+ private static Integer[] toUpperCase ( Integer[] sa ) {
+ assert sa != null;
+ for ( int i = 0, n = sa.length; i < n; i++ ) {
+ Integer s = sa [ i ];
+ sa [ i ] = Character.toUpperCase ( s );
+ }
+ return sa;
+ }
+
+ private static Integer[] toLowerCase ( Integer[] sa ) {
+ assert sa != null;
+ for ( int i = 0, n = sa.length; i < n; i++ ) {
+ Integer s = sa [ i ];
+ sa [ i ] = Character.toLowerCase ( s );
+ }
+ return sa;
+ }
+
+ /* not yet used
+ private static Integer[] toTitleCase ( Integer[] sa ) {
+ assert sa != null;
+ if ( sa.length > 0 ) {
+ sa [ 0 ] = Character.toTitleCase ( sa [ 0 ] );
+ }
+ return sa;
+ }
+ */
+
+ private static List<String> convertWordCase ( List<String> words, int caseType ) {
+ List<String> wl = new ArrayList<String>();
+ for ( String w : words ) {
+ wl.add ( convertWordCase ( w, caseType ) );
+ }
+ return wl;
+ }
+
+ private static String convertWordCase ( String word, int caseType ) {
+ if ( caseType == Character.UPPERCASE_LETTER ) {
+ return word.toUpperCase();
+ } else if ( caseType == Character.LOWERCASE_LETTER ) {
+ return word.toLowerCase();
+ } else if ( caseType == Character.TITLECASE_LETTER ) {
+ StringBuffer sb = new StringBuffer();
+ for ( int i = 0, n = word.length(); i < n; i++ ) {
+ String s = word.substring ( i, i + 1 );
+ if ( i == 0 ) {
+ sb.append ( s.toUpperCase() );
+ } else {
+ sb.append ( s.toLowerCase() );
+ }
+ }
+ return sb.toString();
+ } else {
+ return word;
+ }
+ }
+
+ private static String joinWords ( List<String> words, String separator ) {
+ StringBuffer sb = new StringBuffer();
+ for ( String w : words ) {
+ if ( sb.length() > 0 ) {
+ sb.append ( separator );
+ }
+ sb.append ( w );
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Special number formatter.
+ */
+ interface SpecialNumberFormatter {
+ /**
+ * Format number with special numeral system.
+ * @param number to be formatted
+ * @param one unicode scalar value denoting numeric value 1
+ * @param letterValue letter value (must be one of the above letter value enumeration values)
+ * @param features features (feature sub-parameters)
+ * @param language denotes applicable language
+ * @param country denotes applicable country
+ * @return formatted number as array of unicode scalars
+ */
+ Integer[] format ( long number, int one, int letterValue, String features, String language, String country );
+ }
+
+ /**
+ * English Word Numerals
+ */
+ private static String[] englishWordOnes = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
+ private static String[] englishWordTeens = { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" };
+ private static String[] englishWordTens = { "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" };
+ private static String[] englishWordOthers = { "hundred", "thousand", "million", "billion" };
+ private static String[] englishWordOnesOrd = { "none", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth" };
+ private static String[] englishWordTeensOrd = { "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth" };
+ private static String[] englishWordTensOrd = { "", "tenth", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetith" };
+ private static String[] englishWordOthersOrd = { "hundredth", "thousandth", "millionth", "billionth" };
+ private static class EnglishNumberAsWordFormatter implements SpecialNumberFormatter {
+ private int caseType = Character.UPPERCASE_LETTER;
+ EnglishNumberAsWordFormatter ( int caseType ) {
+ this.caseType = caseType;
+ }
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ List<String> wl = new ArrayList<String>();
+ if ( number >= 1000000000000L ) {
+ return null;
+ } else {
+ boolean ordinal = hasFeature ( features, "ordinal" );
+ if ( number == 0 ) {
+ wl.add ( englishWordOnes [ 0 ] );
+ } else if ( ordinal && ( number < 10 ) ) {
+ wl.add ( englishWordOnesOrd [ (int) number ] );
+ } else {
+ int ones = (int) ( number % 1000 );
+ int thousands = (int) ( ( number / 1000 ) % 1000 );
+ int millions = (int) ( ( number / 1000000 ) % 1000 );
+ int billions = (int) ( ( number / 1000000000 ) % 1000 );
+ if ( billions > 0 ) {
+ wl = formatOnesInThousand ( wl, billions );
+ if ( ordinal && ( ( number % 1000000000 ) == 0 ) ) {
+ wl.add ( englishWordOthersOrd[3] );
+ } else {
+ wl.add ( englishWordOthers[3] );
+ }
+ }
+ if ( millions > 0 ) {
+ wl = formatOnesInThousand ( wl, millions );
+ if ( ordinal && ( ( number % 1000000 ) == 0 ) ) {
+ wl.add ( englishWordOthersOrd[2] );
+ } else {
+ wl.add ( englishWordOthers[2] );
+ }
+ }
+ if ( thousands > 0 ) {
+ wl = formatOnesInThousand ( wl, thousands );
+ if ( ordinal && ( ( number % 1000 ) == 0 ) ) {
+ wl.add ( englishWordOthersOrd[1] );
+ } else {
+ wl.add ( englishWordOthers[1] );
+ }
+ }
+ if ( ones > 0 ) {
+ wl = formatOnesInThousand ( wl, ones, ordinal );
+ }
+ }
+ wl = convertWordCase ( wl, caseType );
+ return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true );
+ }
+ }
+ private List<String> formatOnesInThousand ( List<String> wl, int number ) {
+ return formatOnesInThousand ( wl, number, false );
+ }
+ private List<String> formatOnesInThousand ( List<String> wl, int number, boolean ordinal ) {
+ assert number < 1000;
+ int ones = number % 10;
+ int tens = ( number / 10 ) % 10;
+ int hundreds = ( number / 100 ) % 10;
+ if ( hundreds > 0 ) {
+ wl.add ( englishWordOnes [ hundreds ] );
+ if ( ordinal && ( ( number % 100 ) == 0 ) ) {
+ wl.add ( englishWordOthersOrd[0] );
+ } else {
+ wl.add ( englishWordOthers[0] );
+ }
+ }
+ if ( tens > 0 ) {
+ if ( tens == 1 ) {
+ if ( ordinal ) {
+ wl.add ( englishWordTeensOrd [ ones ] );
+ } else {
+ wl.add ( englishWordTeens [ ones ] );
+ }
+ } else {
+ if ( ordinal && ( ones == 0 ) ) {
+ wl.add ( englishWordTensOrd [ tens ] );
+ } else {
+ wl.add ( englishWordTens [ tens ] );
+ }
+ if ( ones > 0 ) {
+ if ( ordinal ) {
+ wl.add ( englishWordOnesOrd [ ones ] );
+ } else {
+ wl.add ( englishWordOnes [ ones ] );
+ }
+ }
+ }
+ } else if ( ones > 0 ) {
+ if ( ordinal ) {
+ wl.add ( englishWordOnesOrd [ ones ] );
+ } else {
+ wl.add ( englishWordOnes [ ones ] );
+ }
+ }
+ return wl;
+ }
+ }
+
+ /**
+ * French Word Numerals
+ */
+ private static String[] frenchWordOnes = { "z\u00e9ro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf" };
+ private static String[] frenchWordTeens = { "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" };
+ private static String[] frenchWordTens = { "", "dix", "vingt", "trente", "quarante", "cinquante", "soixante", "soixante-dix", "quatre-vingt", "quatre-vingt-dix" };
+ private static String[] frenchWordOthers = { "cent", "cents", "mille", "million", "millions", "milliard", "milliards" };
+ private static String[] frenchWordOnesOrdMale = { "premier", "deuxi\u00e8me", "troisi\u00e8me", "quatri\u00e8me", "cinqui\u00e8me", "sixi\u00e8me", "septi\u00e8me", "huiti\u00e8me", "neuvi\u00e8me", "dixi\u00e8me" };
+ private static String[] frenchWordOnesOrdFemale = { "premi\u00e8re", "deuxi\u00e8me", "troisi\u00e8me", "quatri\u00e8me", "cinqui\u00e8me", "sixi\u00e8me", "septi\u00e8me", "huiti\u00e8me", "neuvi\u00e8me", "dixi\u00e8me" };
+ private static class FrenchNumberAsWordFormatter implements SpecialNumberFormatter {
+ private int caseType = Character.UPPERCASE_LETTER;
+ FrenchNumberAsWordFormatter ( int caseType ) {
+ this.caseType = caseType;
+ }
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ List<String> wl = new ArrayList<String>();
+ if ( number >= 1000000000000L ) {
+ return null;
+ } else {
+ boolean ordinal = hasFeature ( features, "ordinal" );
+ if ( number == 0 ) {
+ wl.add ( frenchWordOnes [ 0 ] );
+ } else if ( ordinal && ( number <= 10 ) ) {
+ boolean female = hasFeature ( features, "female" );
+ if ( female ) {
+ wl.add ( frenchWordOnesOrdFemale [ (int) number ] );
+ } else {
+ wl.add ( frenchWordOnesOrdMale [ (int) number ] );
+ }
+ } else {
+ int ones = (int) ( number % 1000 );
+ int thousands = (int) ( ( number / 1000 ) % 1000 );
+ int millions = (int) ( ( number / 1000000 ) % 1000 );
+ int billions = (int) ( ( number / 1000000000 ) % 1000 );
+ if ( billions > 0 ) {
+ wl = formatOnesInThousand ( wl, billions );
+ if ( billions == 1 ) {
+ wl.add ( frenchWordOthers[5] );
+ } else {
+ wl.add ( frenchWordOthers[6] );
+ }
+ }
+ if ( millions > 0 ) {
+ wl = formatOnesInThousand ( wl, millions );
+ if ( millions == 1 ) {
+ wl.add ( frenchWordOthers[3] );
+ } else {
+ wl.add ( frenchWordOthers[4] );
+ }
+ }
+ if ( thousands > 0 ) {
+ if ( thousands > 1 ) {
+ wl = formatOnesInThousand ( wl, thousands );
+ }
+ wl.add ( frenchWordOthers[2] );
+ }
+ if ( ones > 0 ) {
+ wl = formatOnesInThousand ( wl, ones );
+ }
+ }
+ wl = convertWordCase ( wl, caseType );
+ return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true );
+ }
+ }
+ private List<String> formatOnesInThousand ( List<String> wl, int number ) {
+ assert number < 1000;
+ int ones = number % 10;
+ int tens = ( number / 10 ) % 10;
+ int hundreds = ( number / 100 ) % 10;
+ if ( hundreds > 0 ) {
+ if ( hundreds > 1 ) {
+ wl.add ( frenchWordOnes [ hundreds ] );
+ }
+ if ( ( hundreds > 1 ) && ( tens == 0 ) && ( ones == 0 ) ) {
+ wl.add ( frenchWordOthers[1] );
+ } else {
+ wl.add ( frenchWordOthers[0] );
+ }
+ }
+ if ( tens > 0 ) {
+ if ( tens == 1 ) {
+ wl.add ( frenchWordTeens [ ones ] );
+ } else if ( tens < 7 ) {
+ if ( ones == 1 ) {
+ wl.add ( frenchWordTens [ tens ] );
+ wl.add ( "et" );
+ wl.add ( frenchWordOnes [ ones ] );
+ } else {
+ StringBuffer sb = new StringBuffer();
+ sb.append ( frenchWordTens [ tens ] );
+ if ( ones > 0 ) {
+ sb.append ( '-' );
+ sb.append ( frenchWordOnes [ ones ] );
+ }
+ wl.add ( sb.toString() );
+ }
+ } else if ( tens == 7 ) {
+ if ( ones == 1 ) {
+ wl.add ( frenchWordTens [ 6 ] );
+ wl.add ( "et" );
+ wl.add ( frenchWordTeens [ ones ] );
+ } else {
+ StringBuffer sb = new StringBuffer();
+ sb.append ( frenchWordTens [ 6 ] );
+ sb.append ( '-' );
+ sb.append ( frenchWordTeens [ ones ] );
+ wl.add ( sb.toString() );
+ }
+ } else if ( tens == 8 ) {
+ StringBuffer sb = new StringBuffer();
+ sb.append ( frenchWordTens [ tens ] );
+ if ( ones > 0 ) {
+ sb.append ( '-' );
+ sb.append ( frenchWordOnes [ ones ] );
+ } else {
+ sb.append ( 's' );
+ }
+ wl.add ( sb.toString() );
+ } else if ( tens == 9 ) {
+ StringBuffer sb = new StringBuffer();
+ sb.append ( frenchWordTens [ 8 ] );
+ sb.append ( '-' );
+ sb.append ( frenchWordTeens [ ones ] );
+ wl.add ( sb.toString() );
+ }
+ } else if ( ones > 0 ) {
+ wl.add ( frenchWordOnes [ ones ] );
+ }
+ return wl;
+ }
+ }
+
+ /**
+ * Spanish Word Numerals
+ */
+ private static String[] spanishWordOnes = { "cero", "uno", "dos", "tres", "cuatro", "cinco", "seise", "siete", "ocho", "nueve" };
+ private static String[] spanishWordTeens = { "diez", "once", "doce", "trece", "catorce", "quince", "diecis\u00e9is", "diecisiete", "dieciocho", "diecinueve" };
+ private static String[] spanishWordTweens = { "veinte", "veintiuno", "veintid\u00f3s", "veintitr\u00e9s", "veinticuatro", "veinticinco", "veintis\u00e9is", "veintisiete", "veintiocho", "veintinueve" };
+ private static String[] spanishWordTens = { "", "diez", "veinte", "treinta", "cuarenta", "cincuenta", "sesenta", "setenta", "ochenta", "noventa" };
+ private static String[] spanishWordHundreds = { "", "ciento", "doscientos", "trescientos", "cuatrocientos", "quinientos", "seiscientos", "setecientos", "ochocientos", "novecientos" };
+ private static String[] spanishWordOthers = { "un", "cien", "mil", "mill\u00f3n", "millones" };
+ private static String[] spanishWordOnesOrdMale = { "ninguno", "primero", "segundo", "tercero", "cuarto", "quinto", "sexto", "s\u00e9ptimo", "octavo", "novento", "d\u00e9cimo" };
+ private static String[] spanishWordOnesOrdFemale = { "ninguna", "primera", "segunda", "tercera", "cuarta", "quinta", "sexta", "s\u00e9ptima", "octava", "noventa", "d\u00e9cima" };
+ private static class SpanishNumberAsWordFormatter implements SpecialNumberFormatter {
+ private int caseType = Character.UPPERCASE_LETTER;
+ SpanishNumberAsWordFormatter ( int caseType ) {
+ this.caseType = caseType;
+ }
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ List<String> wl = new ArrayList<String>();
+ if ( number >= 1000000000000L ) {
+ return null;
+ } else {
+ boolean ordinal = hasFeature ( features, "ordinal" );
+ if ( number == 0 ) {
+ wl.add ( spanishWordOnes [ 0 ] );
+ } else if ( ordinal && ( number <= 10 ) ) {
+ boolean female = hasFeature ( features, "female" );
+ if ( female ) {
+ wl.add ( spanishWordOnesOrdFemale [ (int) number ] );
+ } else {
+ wl.add ( spanishWordOnesOrdMale [ (int) number ] );
+ }
+ } else {
+ int ones = (int) ( number % 1000 );
+ int thousands = (int) ( ( number / 1000 ) % 1000 );
+ int millions = (int) ( ( number / 1000000 ) % 1000 );
+ int billions = (int) ( ( number / 1000000000 ) % 1000 );
+ if ( billions > 0 ) {
+ if ( billions > 1 ) {
+ wl = formatOnesInThousand ( wl, billions );
+ }
+ wl.add ( spanishWordOthers[2] );
+ wl.add ( spanishWordOthers[4] );
+ }
+ if ( millions > 0 ) {
+ if ( millions == 1 ) {
+ wl.add ( spanishWordOthers[0] );
+ } else {
+ wl = formatOnesInThousand ( wl, millions );
+ }
+ if ( millions > 1 ) {
+ wl.add ( spanishWordOthers[4] );
+ } else {
+ wl.add ( spanishWordOthers[3] );
+ }
+ }
+ if ( thousands > 0 ) {
+ if ( thousands > 1 ) {
+ wl = formatOnesInThousand ( wl, thousands );
+ }
+ wl.add ( spanishWordOthers[2] );
+ }
+ if ( ones > 0 ) {
+ wl = formatOnesInThousand ( wl, ones );
+ }
+ }
+ wl = convertWordCase ( wl, caseType );
+ return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true );
+ }
+ }
+ private List<String> formatOnesInThousand ( List<String> wl, int number ) {
+ assert number < 1000;
+ int ones = number % 10;
+ int tens = ( number / 10 ) % 10;
+ int hundreds = ( number / 100 ) % 10;
+ if ( hundreds > 0 ) {
+ if ( ( hundreds == 1 ) && ( tens == 0 ) && ( ones == 0 ) ) {
+ wl.add ( spanishWordOthers[1] );
+ } else {
+ wl.add ( spanishWordHundreds [ hundreds ] );
+ }
+ }
+ if ( tens > 0 ) {
+ if ( tens == 1 ) {
+ wl.add ( spanishWordTeens [ ones ] );
+ } else if ( tens == 2 ) {
+ wl.add ( spanishWordTweens [ ones ] );
+ } else {
+ wl.add ( spanishWordTens [ tens ] );
+ if ( ones > 0 ) {
+ wl.add ( "y" );
+ wl.add ( spanishWordOnes [ ones ] );
+ }
+ }
+ } else if ( ones > 0 ) {
+ wl.add ( spanishWordOnes [ ones ] );
+ }
+ return wl;
+ }
+ }
+
+ /**
+ * Roman (Latin) Numerals
+ */
+ private static int[] romanMapping = {
+ 100000,
+ 90000,
+ 50000,
+ 40000,
+ 10000,
+ 9000,
+ 5000,
+ 4000,
+ 1000,
+ 900,
+ 500,
+ 400,
+ 100,
+ 90,
+ 50,
+ 40,
+ 10,
+ 9,
+ 8,
+ 7,
+ 6,
+ 5,
+ 4,
+ 3,
+ 2,
+ 1
+ };
+ private static String[] romanStandardForms = {
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "m",
+ "cm",
+ "d",
+ "cd",
+ "c",
+ "xc",
+ "l",
+ "xl",
+ "x",
+ "ix",
+ null,
+ null,
+ null,
+ "v",
+ "iv",
+ null,
+ null,
+ "i"
+ };
+ private static String[] romanLargeForms = {
+ "\u2188",
+ "\u2182\u2188",
+ "\u2187",
+ "\u2182\u2187",
+ "\u2182",
+ "\u2180\u2182",
+ "\u2181",
+ "\u2180\u2181",
+ "m",
+ "cm",
+ "d",
+ "cd",
+ "c",
+ "xc",
+ "l",
+ "xl",
+ "x",
+ "ix",
+ null,
+ null,
+ null,
+ "v",
+ "iv",
+ null,
+ null,
+ "i"
+ };
+ private static String[] romanNumberForms = {
+ "\u2188",
+ "\u2182\u2188",
+ "\u2187",
+ "\u2182\u2187",
+ "\u2182",
+ "\u2180\u2182",
+ "\u2181",
+ "\u2180\u2181",
+ "\u216F",
+ "\u216D\u216F",
+ "\u216E",
+ "\u216D\u216E",
+ "\u216D",
+ "\u2169\u216D",
+ "\u216C",
+ "\u2169\u216C",
+ "\u2169",
+ "\u2168",
+ "\u2167",
+ "\u2166",
+ "\u2165",
+ "\u2164",
+ "\u2163",
+ "\u2162",
+ "\u2161",
+ "\u2160"
+ };
+ private static class RomanNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ List<Integer> sl = new ArrayList<Integer>();
+ if ( number == 0 ) {
+ return null;
+ } else {
+ String[] forms;
+ int maxNumber;
+ if ( hasFeature ( features, "unicode-number-forms" ) ) {
+ forms = romanNumberForms;
+ maxNumber = 199999;
+ } else if ( hasFeature ( features, "large" ) ) {
+ forms = romanLargeForms;
+ maxNumber = 199999;
+ } else {
+ forms = romanStandardForms;
+ maxNumber = 4999;
+ }
+ if ( number > maxNumber ) {
+ return null;
+ } else {
+ while ( number > 0 ) {
+ for ( int i = 0, n = romanMapping.length; i < n; i++ ) {
+ int d = romanMapping [ i ];
+ if ( ( number >= d ) && ( forms [ i ] != null ) ) {
+ appendScalars ( sl, UTF32.toUTF32 ( forms [ i ], 0, true ) );
+ number = number - d;
+ break;
+ }
+ }
+ }
+ if ( one == (int) 'I' ) {
+ return toUpperCase ( sl.toArray ( new Integer [ sl.size() ] ) );
+ } else if ( one == (int) 'i' ) {
+ return toLowerCase ( sl.toArray ( new Integer [ sl.size() ] ) );
+ } else {
+ return null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Isopsephry (Greek) Numerals
+ */
+ private static class IsopsephryNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ return null;
+ }
+ }
+
+ /**
+ * Gematria (Hebrew) Numerals
+ */
+ private static int[] hebrewGematriaAlphabeticMap = {
+ // ones
+ 0x05D0, // ALEF
+ 0x05D1, // BET
+ 0x05D2, // GIMEL
+ 0x05D3, // DALET
+ 0x05D4, // HE
+ 0x05D5, // VAV
+ 0x05D6, // ZAYIN
+ 0x05D7, // HET
+ 0x05D8, // TET
+ // tens
+ 0x05D9, // YOD
+ 0x05DB, // KAF
+ 0x05DC, // LAMED
+ 0x05DE, // MEM
+ 0x05E0, // NUN
+ 0x05E1, // SAMEKH
+ 0x05E2, // AYIN
+ 0x05E4, // PE
+ 0x05E6, // TSADHI
+ // hundreds
+ 0x05E7, // QOF
+ 0x05E8, // RESH
+ 0x05E9, // SHIN
+ 0x05EA, // TAV
+ 0x05DA, // FINAL KAF
+ 0x05DD, // FINAL MEM
+ 0x05DF, // FINAL NUN
+ 0x05E3, // FINAL PE
+ 0x05E5, // FINAL TSADHI
+ };
+ private class GematriaNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ if ( one == 0x05D0 ) {
+ if ( letterValue == LETTER_VALUE_ALPHABETIC ) {
+ return formatNumberAsSequence ( number, one, hebrewGematriaAlphabeticMap.length, hebrewGematriaAlphabeticMap );
+ } else if ( letterValue == LETTER_VALUE_TRADITIONAL ) {
+ if ( ( number == 0 ) || ( number > 1999 ) ) {
+ return null;
+ } else {
+ return formatAsGematriaNumber ( number, features, language, country );
+ }
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ private Integer[] formatAsGematriaNumber ( long number, String features, String language, String country ) {
+ List<Integer> sl = new ArrayList<Integer>();
+ assert hebrewGematriaAlphabeticMap.length == 27;
+ assert hebrewGematriaAlphabeticMap[0] == 0x05D0; // ALEF
+ assert hebrewGematriaAlphabeticMap[21] == 0x05EA; // TAV
+ assert number != 0;
+ assert number < 2000;
+ int[] map = hebrewGematriaAlphabeticMap;
+ int thousands = (int) ( ( number / 1000 ) % 10 );
+ int hundreds = (int) ( ( number / 100 ) % 10 );
+ int tens = (int) ( ( number / 10 ) % 10 );
+ int ones = (int) ( ( number / 1 ) % 10 );
+ if ( thousands > 0 ) {
+ sl.add ( map [ 0 + ( thousands - 1 ) ] );
+ sl.add ( 0x05F3 );
+ }
+ if ( hundreds > 0 ) {
+ assert hundreds < 10;
+ if ( hundreds < 5 ) {
+ sl.add ( map [ 18 + ( hundreds - 1 ) ] );
+ } else if ( hundreds < 9 ) {
+ sl.add ( map [ 18 + ( 4 - 1 ) ] );
+ sl.add ( 0x05F4 );
+ sl.add ( map [ 18 + ( hundreds - 5 ) ] );
+ } else if ( hundreds == 9 ) {
+ sl.add ( map [ 18 + ( 4 - 1 ) ] );
+ sl.add ( map [ 18 + ( 4 - 1 ) ] );
+ sl.add ( 0x05F4 );
+ sl.add ( map [ 18 + ( hundreds - 9 ) ] );
+ }
+ }
+ if ( number == 15 ) {
+ sl.add ( map [ 9 - 1] );
+ sl.add ( 0x05F4 );
+ sl.add ( map [ 6 - 1] );
+ } else if ( number == 16 ) {
+ sl.add ( map [ 9 - 1 ] );
+ sl.add ( 0x05F4 );
+ sl.add ( map [ 7 - 1 ] );
+ } else {
+ if ( tens > 0 ) {
+ assert tens < 10;
+ sl.add ( map [ 9 + ( tens - 1 ) ] );
+ }
+ if ( ones > 0 ) {
+ assert ones < 10;
+ sl.add ( map [ 0 + ( ones - 1 ) ] );
+ }
+ }
+ return sl.toArray ( new Integer [ sl.size() ] );
+ }
+ }
+
+ /**
+ * Arabic Numerals
+ */
+ private static int[] arabicAbjadiAlphabeticMap = {
+ // ones
+ 0x0623, // ALEF WITH HAMZA ABOVE
+ 0x0628, // BEH
+ 0x062C, // JEEM
+ 0x062F, // DAL
+ 0x0647, // HEH
+ 0x0648, // WAW
+ 0x0632, // ZAIN
+ 0x062D, // HAH
+ 0x0637, // TAH
+ // tens
+ 0x0649, // ALEF MAQSURA
+ 0x0643, // KAF
+ 0x0644, // LAM
+ 0x0645, // MEEM
+ 0x0646, // NOON
+ 0x0633, // SEEN
+ 0x0639, // AIN
+ 0x0641, // FEH
+ 0x0635, // SAD
+ // hundreds
+ 0x0642, // QAF
+ 0x0631, // REH
+ 0x0634, // SHEEN
+ 0x062A, // TEH
+ 0x062B, // THEH
+ 0x062E, // KHAH
+ 0x0630, // THAL
+ 0x0636, // DAD
+ 0x0638, // ZAH
+ // thousands
+ 0x063A, // GHAIN
+ };
+ private static int[] arabicHijaiAlphabeticMap = {
+ 0x0623, // ALEF WITH HAMZA ABOVE
+ 0x0628, // BEH
+ 0x062A, // TEH
+ 0x062B, // THEH
+ 0x062C, // JEEM
+ 0x062D, // HAH
+ 0x062E, // KHAH
+ 0x062F, // DAL
+ 0x0630, // THAL
+ 0x0631, // REH
+ 0x0632, // ZAIN
+ 0x0633, // SEEN
+ 0x0634, // SHEEN
+ 0x0635, // SAD
+ 0x0636, // DAD
+ 0x0637, // TAH
+ 0x0638, // ZAH
+ 0x0639, // AIN
+ 0x063A, // GHAIN
+ 0x0641, // FEH
+ 0x0642, // QAF
+ 0x0643, // KAF
+ 0x0644, // LAM
+ 0x0645, // MEEM
+ 0x0646, // NOON
+ 0x0647, // HEH
+ 0x0648, // WAW
+ 0x0649, // ALEF MAQSURA
+ };
+ private class ArabicNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ if ( one == 0x0627 ) {
+ int[] map;
+ if ( letterValue == LETTER_VALUE_TRADITIONAL ) {
+ map = arabicAbjadiAlphabeticMap;
+ } else if ( letterValue == LETTER_VALUE_ALPHABETIC ) {
+ map = arabicHijaiAlphabeticMap;
+ } else {
+ map = arabicAbjadiAlphabeticMap;
+ }
+ return formatNumberAsSequence ( number, one, map.length, map );
+ } else if ( one == 0x0623 ) {
+ if ( ( number == 0 ) || ( number > 1999 ) ) {
+ return null;
+ } else {
+ return formatAsAbjadiNumber ( number, features, language, country );
+ }
+ } else {
+ return null;
+ }
+ }
+ private Integer[] formatAsAbjadiNumber ( long number, String features, String language, String country ) {
+ List<Integer> sl = new ArrayList<Integer>();
+ assert arabicAbjadiAlphabeticMap.length == 28;
+ assert arabicAbjadiAlphabeticMap[0] == 0x0623; // ALEF WITH HAMZA ABOVE
+ assert arabicAbjadiAlphabeticMap[27] == 0x063A; // GHAIN
+ assert number != 0;
+ assert number < 2000;
+ int[] map = arabicAbjadiAlphabeticMap;
+ int thousands = (int) ( ( number / 1000 ) % 10 );
+ int hundreds = (int) ( ( number / 100 ) % 10 );
+ int tens = (int) ( ( number / 10 ) % 10 );
+ int ones = (int) ( ( number / 1 ) % 10 );
+ if ( thousands > 0 ) {
+ assert thousands < 2;
+ sl.add ( map [ 27 + ( thousands - 1 ) ] );
+ }
+ if ( hundreds > 0 ) {
+ assert thousands < 10;
+ sl.add ( map [ 18 + ( hundreds - 1 ) ] );
+ }
+ if ( tens > 0 ) {
+ assert tens < 10;
+ sl.add ( map [ 9 + ( tens - 1 ) ] );
+ }
+ if ( ones > 0 ) {
+ assert ones < 10;
+ sl.add ( map [ 0 + ( ones - 1 ) ] );
+ }
+ return sl.toArray ( new Integer [ sl.size() ] );
+ }
+ }
+
+ /**
+ * Kana (Japanese) Numerals
+ */
+ private static int[] hiraganaGojuonAlphabeticMap = {
+ 0x3042, // A
+ 0x3044, // I
+ 0x3046, // U
+ 0x3048, // E
+ 0x304A, // O
+ 0x304B, // KA
+ 0x304D, // KI
+ 0x304F, // KU
+ 0x3051, // KE
+ 0x3053, // KO
+ 0x3055, // SA
+ 0x3057, // SI
+ 0x3059, // SU
+ 0x305B, // SE
+ 0x305D, // SO
+ 0x305F, // TA
+ 0x3061, // TI
+ 0x3064, // TU
+ 0x3066, // TE
+ 0x3068, // TO
+ 0x306A, // NA
+ 0x306B, // NI
+ 0x306C, // NU
+ 0x306D, // NE
+ 0x306E, // NO
+ 0x306F, // HA
+ 0x3072, // HI
+ 0x3075, // HU
+ 0x3078, // HE
+ 0x307B, // HO
+ 0x307E, // MA
+ 0x307F, // MI
+ 0x3080, // MU
+ 0x3081, // ME
+ 0x3082, // MO
+ 0x3084, // YA
+ 0x3086, // YU
+ 0x3088, // YO
+ 0x3089, // RA
+ 0x308A, // RI
+ 0x308B, // RU
+ 0x308C, // RE
+ 0x308D, // RO
+ 0x308F, // WA
+ 0x3090, // WI
+ 0x3091, // WE
+ 0x3092, // WO
+ 0x3093, // N
+ };
+ private static int[] katakanaGojuonAlphabeticMap = {
+ 0x30A2, // A
+ 0x30A4, // I
+ 0x30A6, // U
+ 0x30A8, // E
+ 0x30AA, // O
+ 0x30AB, // KA
+ 0x30AD, // KI
+ 0x30AF, // KU
+ 0x30B1, // KE
+ 0x30B3, // KO
+ 0x30B5, // SA
+ 0x30B7, // SI
+ 0x30B9, // SU
+ 0x30BB, // SE
+ 0x30BD, // SO
+ 0x30BF, // TA
+ 0x30C1, // TI
+ 0x30C4, // TU
+ 0x30C6, // TE
+ 0x30C8, // TO
+ 0x30CA, // NA
+ 0x30CB, // NI
+ 0x30CC, // NU
+ 0x30CD, // NE
+ 0x30CE, // NO
+ 0x30CF, // HA
+ 0x30D2, // HI
+ 0x30D5, // HU
+ 0x30D8, // HE
+ 0x30DB, // HO
+ 0x30DE, // MA
+ 0x30DF, // MI
+ 0x30E0, // MU
+ 0x30E1, // ME
+ 0x30E2, // MO
+ 0x30E4, // YA
+ 0x30E6, // YU
+ 0x30E8, // YO
+ 0x30E9, // RA
+ 0x30EA, // RI
+ 0x30EB, // RU
+ 0x30EC, // RE
+ 0x30ED, // RO
+ 0x30EF, // WA
+ 0x30F0, // WI
+ 0x30F1, // WE
+ 0x30F2, // WO
+ 0x30F3, // N
+ };
+ private class KanaNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ if ( ( one == 0x3042 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) {
+ return formatNumberAsSequence ( number, one, hiraganaGojuonAlphabeticMap.length, hiraganaGojuonAlphabeticMap );
+ } else if ( ( one == 0x30A2 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) {
+ return formatNumberAsSequence ( number, one, katakanaGojuonAlphabeticMap.length, katakanaGojuonAlphabeticMap );
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Thai Numerals
+ */
+ private static int[] thaiAlphabeticMap = {
+ 0x0E01,
+ 0x0E02,
+ 0x0E03,
+ 0x0E04,
+ 0x0E05,
+ 0x0E06,
+ 0x0E07,
+ 0x0E08,
+ 0x0E09,
+ 0x0E0A,
+ 0x0E0B,
+ 0x0E0C,
+ 0x0E0D,
+ 0x0E0E,
+ 0x0E0F,
+ 0x0E10,
+ 0x0E11,
+ 0x0E12,
+ 0x0E13,
+ 0x0E14,
+ 0x0E15,
+ 0x0E16,
+ 0x0E17,
+ 0x0E18,
+ 0x0E19,
+ 0x0E1A,
+ 0x0E1B,
+ 0x0E1C,
+ 0x0E1D,
+ 0x0E1E,
+ 0x0E1F,
+ 0x0E20,
+ 0x0E21,
+ 0x0E22,
+ 0x0E23,
+ // 0x0E24, // RU - not used in modern sequence
+ 0x0E25,
+ // 0x0E26, // LU - not used in modern sequence
+ 0x0E27,
+ 0x0E28,
+ 0x0E29,
+ 0x0E2A,
+ 0x0E2B,
+ 0x0E2C,
+ 0x0E2D,
+ 0x0E2E,
+ };
+ private class ThaiNumeralsFormatter implements SpecialNumberFormatter {
+ @Override
+ public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) {
+ if ( ( one == 0x0E01 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) {
+ return formatNumberAsSequence ( number, one, thaiAlphabeticMap.length, thaiAlphabeticMap );
+ } else {
+ return null;
+ }
+ }
+ }
+
+}
Added: xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java
URL: http://svn.apache.org/viewvc/xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java?rev=1293736&view=auto
==============================================================================
--- xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java (added)
+++ xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java Sun Feb 26 02:29:01 2012
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.complexscripts.util;
+
+/**
+ * Interface for providing script specific context testers.
+ * @author Glenn Adams
+ */
+public interface ScriptContextTester {
+
+ /**
+ * Obtain a glyph context tester for the specified feature.
+ * @param feature a feature identifier
+ * @return a glyph context tester or null if none available for the specified feature
+ */
+ GlyphContextTester getTester ( String feature );
+
+}
Added: xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/UTF32.java
URL: http://svn.apache.org/viewvc/xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/UTF32.java?rev=1293736&view=auto
==============================================================================
--- xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/UTF32.java (added)
+++ xmlgraphics/fop/trunk/src/java/org/apache/fop/complexscripts/util/UTF32.java Sun Feb 26 02:29:01 2012
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.complexscripts.util;
+
+import org.apache.fop.util.CharUtilities;
+
+// CSOFF: InnerAssignmentCheck
+
+/**
+ * UTF32 related utilities.
+ * @author Glenn Adams
+ */
+public final class UTF32 {
+
+ private UTF32() {
+ }
+
+ /**
+ * Convert Java string (UTF-16) to a Unicode scalar array (UTF-32).
+ * Note that if there are any non-BMP encoded characters present in the
+ * input, then the number of entries in the output array will be less
+ * than the number of elements in the input string. Any
+ * @param s input string
+ * @param substitution value to substitute for ill-formed surrogate
+ * @param errorOnSubstitution throw runtime exception (IllegalArgumentException) in
+ * case this argument is true and a substitution would be attempted
+ * @return output scalar array
+ * @throws IllegalArgumentException if substitution required and errorOnSubstitution
+ * is not false
+ */
+ public static Integer[] toUTF32 ( String s, int substitution, boolean errorOnSubstitution )
+ throws IllegalArgumentException {
+ int n;
+ if ( ( n = s.length() ) == 0 ) {
+ return new Integer[0];
+ } else {
+ Integer[] sa = new Integer [ n ];
+ int k = 0;
+ for ( int i = 0; i < n; i++ ) {
+ int c = (int) s.charAt(i);
+ if ( ( c >= 0xD800 ) && ( c < 0xE000 ) ) {
+ int s1 = c;
+ int s2 = ( ( i + 1 ) < n ) ? (int) s.charAt ( i + 1 ) : 0;
+ if ( s1 < 0xDC00 ) {
+ if ( ( s2 >= 0xDC00 ) && ( s2 < 0xE000 ) ) {
+ c = ( ( s1 - 0xD800 ) << 10 ) + ( s2 - 0xDC00 ) + 65536;
+ i++;
+ } else {
+ if ( errorOnSubstitution ) {
+ throw new IllegalArgumentException
+ ( "isolated high (leading) surrogate" );
+ } else {
+ c = substitution;
+ }
+ }
+ } else {
+ if ( errorOnSubstitution ) {
+ throw new IllegalArgumentException
+ ( "isolated low (trailing) surrogate" );
+ } else {
+ c = substitution;
+ }
+ }
+ }
+ sa[k++] = c;
+ }
+ if ( k == n ) {
+ return sa;
+ } else {
+ Integer[] na = new Integer [ k ];
+ System.arraycopy ( sa, 0, na, 0, k );
+ return na;
+ }
+ }
+ }
+
+ /**
+ * Convert a Unicode scalar array (UTF-32) a Java string (UTF-16).
+ * @param sa input scalar array
+ * @return output (UTF-16) string
+ * @throws IllegalArgumentException if an input scalar value is illegal,
+ * e.g., a surrogate or out of range
+ */
+ public static String fromUTF32 ( Integer[] sa ) throws IllegalArgumentException {
+ StringBuffer sb = new StringBuffer();
+ for ( int s : sa ) {
+ if ( s < 65535 ) {
+ if ( ( s < 0xD800 ) || ( s > 0xDFFF ) ) {
+ sb.append ( (char) s );
+ } else {
+ String ncr = CharUtilities.charToNCRef(s);
+ throw new IllegalArgumentException
+ ( "illegal scalar value 0x" + ncr.substring(2, ncr.length() - 1)
+ + "; cannot be UTF-16 surrogate" );
+ }
+ } else if ( s < 1114112 ) {
+ int s1 = ( ( ( s - 65536 ) >> 10 ) & 0x3FF ) + 0xD800;
+ int s2 = ( ( ( s - 65536 ) >> 0 ) & 0x3FF ) + 0xDC00;
+ sb.append ( (char) s1 );
+ sb.append ( (char) s2 );
+ } else {
+ String ncr = CharUtilities.charToNCRef(s);
+ throw new IllegalArgumentException
+ ( "illegal scalar value 0x" + ncr.substring(2, ncr.length() - 1)
+ + "; out of range for UTF-16" );
+ }
+ }
+ return sb.toString();
+ }
+
+}
Modified: xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/Constants.java
URL: http://svn.apache.org/viewvc/xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/Constants.java?rev=1293736&r1=1293735&r2=1293736&view=diff
==============================================================================
--- xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/Constants.java (original)
+++ xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/Constants.java Sun Feb 26 02:29:01 2012
@@ -26,7 +26,7 @@ package org.apache.fop.fo;
* <li>Input and output formats</li>
* <li>Formatting objects (<em>FO_XXX</em>)</li>
* <li>Formatting properties (<em>PR_XXX</em>)</li>
- * <li>Enumerated values used in formatting properties (<em>EN_XXX</em>)</li>
+ * <li>Enumerated values used in formatting properties and traits (<em>EN_XXX</em>)</li>
* </ul>
*/
public interface Constants {
@@ -778,9 +778,15 @@ public interface Constants {
int PR_X_ALT_TEXT = 274;
/** Property constant - FOP proprietary prototype (in XSL-FO 2.0 Requirements) */
int PR_X_XML_BASE = 275;
+ /**
+ * Property constant - FOP proprietary extension (see NumberConverter) used
+ * to perform additional control over number conversion when generating page
+ * numbers.
+ */
+ int PR_X_NUMBER_CONVERSION_FEATURES = 276;
/** Number of property constants defined */
- int PROPERTY_COUNT = 275;
+ int PROPERTY_COUNT = 276;
// compound property constants
@@ -1208,6 +1214,16 @@ public interface Constants {
int EN_NO_LINK = 199;
/** Enumeration constant -- XSL 1.1 */
int EN_ALTERNATE = 200;
+ /** Enumeration constant -- for *-direction traits */
+ int EN_LR = 201; // left to right
+ /** Enumeration constant -- for *-direction traits */
+ int EN_RL = 202; // right to left
+ /** Enumeration constant -- for *-direction traits */
+ int EN_TB = 203; // top to bottom
+ /** Enumeration constant -- for *-direction traits */
+ int EN_BT = 204; // bottom to top
+ /** Enumeration constant */
+ int EN_TB_LR = 205; // for top-to-bottom, left-to-right writing mode
/** Number of enumeration constants defined */
- int ENUM_COUNT = 200;
+ int ENUM_COUNT = 205;
}
Modified: xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/FONode.java
URL: http://svn.apache.org/viewvc/xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/FONode.java?rev=1293736&r1=1293735&r2=1293736&view=diff
==============================================================================
--- xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/FONode.java (original)
+++ xmlgraphics/fop/trunk/src/java/org/apache/fop/fo/FONode.java Sun Feb 26 02:29:01 2012
@@ -20,8 +20,10 @@
package org.apache.fop.fo;
// Java
+import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
+import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
@@ -35,6 +37,7 @@ import org.apache.xmlgraphics.util.QName
import org.apache.fop.accessibility.StructureTreeElement;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.complexscripts.bidi.DelimitedTextRange;
import org.apache.fop.fo.extensions.ExtensionAttachment;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.extensions.InternalElementMapping;
@@ -865,6 +868,122 @@ public abstract class FONode implements
}
/**
+ * Determine if node has a delimited text range boundary. N.B. that we report
+ * this to be true by default, while specific subclasses override this method to report false.
+ * @param boundary one of {EN_BEFORE, EN_AFTER, or EN_BOTH} enumeration constants
+ * @return true if indicated boundary (or boundaries) constitute a delimited text range
+ * boundary.
+ */
+ public boolean isDelimitedTextRangeBoundary ( int boundary ) {
+ return true;
+ }
+
+ /**
+ * Collect the sequence of delimited text ranges, where each new
+ * range is pushed onto RANGES.
+ * @param ranges a stack of delimited text ranges
+ * @return the (possibly) updated stack of delimited text ranges
+ */
+ public Stack collectDelimitedTextRanges ( Stack ranges ) {
+ // if boundary before, then push new range
+ if ( isRangeBoundaryBefore() ) {
+ maybeNewRange ( ranges );
+ }
+ // get current range, if one exists
+ DelimitedTextRange currentRange;
+ if ( ranges.size() > 0 ) {
+ currentRange = (DelimitedTextRange) ranges.peek();
+ } else {
+ currentRange = null;
+ }
+ // proceses this node
+ ranges = collectDelimitedTextRanges ( ranges, currentRange );
+ // if boundary after, then push new range
+ if ( isRangeBoundaryAfter() ) {
+ maybeNewRange ( ranges );
+ }
+ return ranges;
+ }
+
+ /**
+ * Collect the sequence of delimited text ranges, where each new
+ * range is pushed onto RANGES, where default implementation collects ranges
+ * of child nodes.
+ * @param ranges a stack of delimited text ranges
+ * @param currentRange the current range or null (if none)
+ * @return the (possibly) updated stack of delimited text ranges
+ */
+ protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) {
+ for ( Iterator it = getChildNodes(); ( it != null ) && it.hasNext();) {
+ ranges = ( (FONode) it.next() ).collectDelimitedTextRanges ( ranges );
+ }
+ return ranges;
+ }
+
+ /**
+ * Determine if this node is a new bidi RANGE block item.
+ * @return true if this node is a new bidi RANGE block item
+ */
+ public boolean isBidiRangeBlockItem() {
+ return false;
+ }
+
+ /**
+ * Conditionally add a new delimited text range to RANGES, where new range is
+ * associated with current FONode. A new text range is added unless all of the following are
+ * true:
+ * <ul>
+ * <li>there exists a current range RCUR in RANGES</li>
+ * <li>RCUR is empty</li>
+ * <li>the node of the RCUR is the same node as FN or a descendent node of FN</li>
+ * </ul>
+ * @param ranges stack of delimited text ranges
+ * @return new range (if constructed and pushed onto stack) or current range (if any) or null
+ */
+ private DelimitedTextRange maybeNewRange ( Stack ranges ) {
+ DelimitedTextRange rCur = null; // current range (top of range stack)
+ DelimitedTextRange rNew = null; // new range to be pushed onto range stack
+ if ( ranges.empty() ) {
+ if ( isBidiRangeBlockItem() ) {
+ rNew = new DelimitedTextRange(this);
+ }
+ } else {
+ rCur = (DelimitedTextRange) ranges.peek();
+ if ( rCur != null ) {
+ if ( !rCur.isEmpty() || !isSelfOrDescendent ( rCur.getNode(), this ) ) {
+ rNew = new DelimitedTextRange(this);
+ }
+ }
+ }
+ if ( rNew != null ) {
+ ranges.push ( rNew );
+ } else {
+ rNew = rCur;
+ }
+ return rNew;
+ }
+
+ private boolean isRangeBoundaryBefore() {
+ return isDelimitedTextRangeBoundary ( Constants.EN_BEFORE );
+ }
+
+ private boolean isRangeBoundaryAfter() {
+ return isDelimitedTextRangeBoundary ( Constants.EN_AFTER );
+ }
+
+ /**
+ * Determine if node N2 is the same or a descendent of node N1.
+ */
+ private static boolean isSelfOrDescendent ( FONode n1, FONode n2 ) {
+ for ( FONode n = n2; n != null; n = n.getParent() ) {
+ if ( n == n1 ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Base iterator interface over a FO's children
*/
public interface FONodeIterator extends ListIterator {
---------------------------------------------------------------------
To unsubscribe, e-mail: fop-commits-unsubscribe@xmlgraphics.apache.org
For additional commands, e-mail: fop-commits-help@xmlgraphics.apache.org