aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNarayan Kamath <narayan@google.com>2014-02-27 14:45:37 +0000
committerNarayan Kamath <narayan@google.com>2014-03-03 11:28:59 +0000
commit5d1b0c9f348c25f01c6f2f5be04d2409409c08fe (patch)
tree1d3cfe9711ea9f5a2f6498e4345fdab2ef0ce83e
parent8496be46011538a804aa0a5689ed9563f3c0a01c (diff)
downloadmessageformat-5d1b0c9f348c25f01c6f2f5be04d2409409c08fe.tar.gz
Initial commit: Copy of ICU4J message format / helpers.
This change is an unmodified copy of the following files from the ICU 52.1 distribution. - main/classes/core/src/com/ibm/icu/text/MessageFormat.java - main/classes/core/src/com/ibm/icu/text/MessagePattern.java - main/classes/core/src/com/ibm/icu/impl/PatternProps.java - main/classes/core/src/com/ibm/icu/text/PluralRules.java Note that the code in its current form doesn't compile. Change-Id: Ia449cc0e017083f3f90bf907c993bfc0a88bb988
-rw-r--r--license.html307
-rw-r--r--readme.html1039
-rw-r--r--src/main/com/android/i18n/MessageFormat.java2673
-rw-r--r--src/main/com/android/i18n/MessagePattern.java1612
-rw-r--r--src/main/com/android/i18n/PatternProps.java264
-rw-r--r--src/main/com/android/i18n/PluralRules.java2212
-rw-r--r--unicode-license.txt50
7 files changed, 8157 insertions, 0 deletions
diff --git a/license.html b/license.html
new file mode 100644
index 0000000..94888be
--- /dev/null
+++ b/license.html
@@ -0,0 +1,307 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+<title>ICU License - ICU 1.8.1 and later</title>
+</head>
+
+<body BGCOLOR="#ffffff">
+<h2>ICU License - ICU 1.8.1 and later</h2>
+
+<p>COPYRIGHT AND PERMISSION NOTICE</p>
+
+<p>
+Copyright (c) 1995-2013 International Business Machines Corporation and others
+</p>
+<p>
+All rights reserved.
+</p>
+<p>
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies
+of the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+</p>
+<p>
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL
+THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM,
+OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+USE OR PERFORMANCE OF THIS SOFTWARE.
+</p>
+<p>
+Except as contained in this notice, the name of a copyright holder shall not be
+used in advertising or otherwise to promote the sale, use or other dealings in
+this Software without prior written authorization of the copyright holder.
+</p>
+
+<hr style="color:gray;background-color:gray">
+<p><small>
+All trademarks and registered trademarks mentioned herein are the property of their respective owners.
+</small></p>
+
+<hr style="height:3px;color:black;background-color:black">
+
+<h2>Third-Party Software Licenses</h2>
+This section contains third-party software notices and/or additional terms for licensed
+third-party software components included within ICU libraries.
+
+<h3>1. Unicode Data Files and Software</h3>
+
+<h3 align="center"><a name="Exhibit1">EXHIBIT 1</a><br>
+UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE</h3>
+<blockquote>
+<p>Unicode Data Files include all data files under the directories
+<a href="http://www.unicode.org/Public/">http://www.unicode.org/Public/</a>,
+<a href="http://www.unicode.org/reports/">http://www.unicode.org/reports/</a>,
+and
+<a title="http://www.unicode.org/cldr/data/" onClick="return top.js.OpenExtLink(window,event,this)" target="_blank" href="http://www.unicode.org/cldr/data/">
+http://www.unicode.org/cldr/data/</a>. Unicode Data Files do not include PDF online code charts under the directory <a href="http://www.unicode.org/Public/">http://www.unicode.org/Public/</a>. Software includes any source code
+published in the Unicode Standard or under the directories <a href="http://www.unicode.org/Public/">http://www.unicode.org/Public/</a>,
+<a href="http://www.unicode.org/reports/">http://www.unicode.org/reports/</a>,
+and
+<a title="http://www.unicode.org/cldr/data/" onClick="return top.js.OpenExtLink(window,event,this)" target="_blank" href="http://www.unicode.org/cldr/data/">
+http://www.unicode.org/cldr/data/</a>.</p>
+
+<p>NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.</p>
+<p>COPYRIGHT AND PERMISSION NOTICE</p>
+
+<p>Copyright © 1991-2013 Unicode, Inc. All rights reserved. Distributed under the Terms of Use in
+<a href="http://www.unicode.org/copyright.html">http://www.unicode.org/copyright.html</a>.</p>
+
+<p>Permission is hereby granted, free of charge, to any person obtaining a copy of the Unicode data files and
+any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that (a) the above copyright notice(s) and this permission notice appear
+with all copies of the Data Files or Software, (b) both the above copyright notice(s) and this permission notice appear in associated documentation, and (c) there is clear notice in each modified Data File or in the Software as well as in the documentation associated with the Data File(s) or Software that the data or software has been modified.</p>
+
+<p>THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.</p>
+
+<p>Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.</p>
+
+ <hr width="80%">
+
+<p>Unicode and the Unicode logo are trademarks of Unicode, Inc. in the United States and other countries. All third party trademarks referenced herein are the property of their respective owners.</p>
+
+
+</blockquote>
+
+<h3>2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)</h3>
+<pre>
+ # The Google Chrome software developed by Google is licensed under the BSD license. Other software included in this distribution is provided under other licenses, as set forth below.
+ #
+ # The BSD License
+ # http://opensource.org/licenses/bsd-license.php
+ # Copyright (C) 2006-2008, Google Inc.
+ #
+ # All rights reserved.
+ #
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ #
+ # Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ # Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ #
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #
+ #
+ # The word list in cjdict.txt are generated by combining three word lists listed
+ # below with further processing for compound word breaking. The frequency is generated
+ # with an iterative training against Google web corpora.
+ #
+ # * Libtabe (Chinese)
+ # - https://sourceforge.net/project/?group_id=1519
+ # - Its license terms and conditions are shown below.
+ #
+ # * IPADIC (Japanese)
+ # - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ # - Its license terms and conditions are shown below.
+ #
+ # ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ # /*
+ # * Copyrighy (c) 1999 TaBE Project.
+ # * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ # * All rights reserved.
+ # *
+ # * Redistribution and use in source and binary forms, with or without
+ # * modification, are permitted provided that the following conditions
+ # * are met:
+ # *
+ # * . Redistributions of source code must retain the above copyright
+ # * notice, this list of conditions and the following disclaimer.
+ # * . Redistributions in binary form must reproduce the above copyright
+ # * notice, this list of conditions and the following disclaimer in
+ # * the documentation and/or other materials provided with the
+ # * distribution.
+ # * . Neither the name of the TaBE Project nor the names of its
+ # * contributors may be used to endorse or promote products derived
+ # * from this software without specific prior written permission.
+ # *
+ # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # * OF THE POSSIBILITY OF SUCH DAMAGE.
+ # */
+ #
+ # /*
+ # * Copyright (c) 1999 Computer Systems and Communication Lab,
+ # * Institute of Information Science, Academia Sinica.
+ # * All rights reserved.
+ # *
+ # * Redistribution and use in source and binary forms, with or without
+ # * modification, are permitted provided that the following conditions
+ # * are met:
+ # *
+ # * . Redistributions of source code must retain the above copyright
+ # * notice, this list of conditions and the following disclaimer.
+ # * . Redistributions in binary form must reproduce the above copyright
+ # * notice, this list of conditions and the following disclaimer in
+ # * the documentation and/or other materials provided with the
+ # * distribution.
+ # * . Neither the name of the Computer Systems and Communication Lab
+ # * nor the names of its contributors may be used to endorse or
+ # * promote products derived from this software without specific
+ # * prior written permission.
+ # *
+ # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # * OF THE POSSIBILITY OF SUCH DAMAGE.
+ # */
+ #
+ # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, University of Illinois
+ # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ # ---------------COPYING.libtabe-----END------------------------------------
+ #
+ #
+ # ---------------COPYING.ipadic-----BEGIN------------------------------------
+ #
+ # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ # and Technology. All Rights Reserved.
+ #
+ # Use, reproduction, and distribution of this software is permitted.
+ # Any copy of this software, whether in its original form or modified,
+ # must include both the above copyright notice and the following
+ # paragraphs.
+ #
+ # Nara Institute of Science and Technology (NAIST),
+ # the copyright holders, disclaims all warranties with regard to this
+ # software, including all implied warranties of merchantability and
+ # fitness, in no event shall NAIST be liable for
+ # any special, indirect or consequential damages or any damages
+ # whatsoever resulting from loss of use, data or profits, whether in an
+ # action of contract, negligence or other tortuous action, arising out
+ # of or in connection with the use or performance of this software.
+ #
+ # A large portion of the dictionary entries
+ # originate from ICOT Free Software. The following conditions for ICOT
+ # Free Software applies to the current dictionary as well.
+ #
+ # Each User may also freely distribute the Program, whether in its
+ # original form or modified, to any third party or parties, PROVIDED
+ # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ # on, or be attached to, the Program, which is distributed substantially
+ # in the same form as set out herein and that such intended
+ # distribution, if actually made, will neither violate or otherwise
+ # contravene any of the laws and regulations of the countries having
+ # jurisdiction over the User or the intended distribution itself.
+ #
+ # NO WARRANTY
+ #
+ # The program was produced on an experimental basis in the course of the
+ # research and development conducted during the project and is provided
+ # to users as so produced on an experimental basis. Accordingly, the
+ # program is provided without any warranty whatsoever, whether express,
+ # implied, statutory or otherwise. The term "warranty" used herein
+ # includes, but is not limited to, any warranty of the quality,
+ # performance, merchantability and fitness for a particular purpose of
+ # the program and the nonexistence of any infringement or violation of
+ # any right of any third party.
+ #
+ # Each user of the program will agree and understand, and be deemed to
+ # have agreed and understood, that there is no warranty whatsoever for
+ # the program and, accordingly, the entire risk arising from or
+ # otherwise connected with the program is assumed by the user.
+ #
+ # Therefore, neither ICOT, the copyright holder, or any other
+ # organization that participated in or was otherwise related to the
+ # development of the program and their respective officials, directors,
+ # officers and other employees shall be held liable for any and all
+ # damages, including, without limitation, general, special, incidental
+ # and consequential damages, arising out of or otherwise in connection
+ # with the use or inability to use the program or any product, material
+ # or result produced or otherwise obtained by using the program,
+ # regardless of whether they have been advised of, or otherwise had
+ # knowledge of, the possibility of such damages at any time during the
+ # project or thereafter. Each user will be deemed to have agreed to the
+ # foregoing by his or her commencement of use of the program. The term
+ # "use" as used herein includes, but is not limited to, the use,
+ # modification, copying and distribution of the program and the
+ # production of secondary products from the program.
+ #
+ # In the case where the program, whether in its original form or
+ # modified, was distributed or delivered to or received by a user from
+ # any person, organization or entity other than ICOT, unless it makes or
+ # grants independently of ICOT any specific warranty to the user in
+ # writing, such person, organization or entity, will also be exempted
+ # from and not be held liable to the user for any such damages as noted
+ # above as far as the program is concerned.
+ #
+ # ---------------COPYING.ipadic-----END------------------------------------
+</pre>
+
+<h3>3. Time Zone Database</h3>
+<p>ICU uses the public domain data and code derived from <a href="http://www.iana.org/time-zones">
+Time Zone Database</a> for its time zone support. The ownership of the TZ database is explained
+in <a href="http://tools.ietf.org/html/rfc6557">BCP 175: Procedure for Maintaining the Time Zone
+Database</a> section 7.<p>
+
+<pre>
+7. Database Ownership
+
+ The TZ database itself is not an IETF Contribution or an IETF
+ document. Rather it is a pre-existing and regularly updated work
+ that is in the public domain, and is intended to remain in the public
+ domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do not apply
+ to the TZ Database or contributions that individuals make to it.
+ Should any claims be made and substantiated against the TZ Database,
+ the organization that is providing the IANA Considerations defined in
+ this RFC, under the memorandum of understanding with the IETF,
+ currently ICANN, may act in accordance with all competent court
+ orders. No ownership claims will be made by ICANN or the IETF Trust
+ on the database or the code. Any person making a contribution to the
+ database or code waives all rights to future claims in that
+ contribution or in the TZ Database.
+
+</pre>
+
+
+</body>
+</html>
diff --git a/readme.html b/readme.html
new file mode 100644
index 0000000..a161d14
--- /dev/null
+++ b/readme.html
@@ -0,0 +1,1039 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta http-equiv="Content-Style-Type" content="text/css2">
+ <title>ReadMe for ICU4J</title>
+ <meta name="COPYRIGHT" content="Copyright 2000-2013, International Business Machines Corporation and others. All Rights Reserved.">
+ <style type="text/css">
+h3.doc { background: #CCCCFF }
+h4.doc { text-decoration: underline }
+ </style>
+</head>
+<body style="background-color: rgb(255, 255, 255);" lang="EN-US"
+ link="#0000ff" vlink="#800080">
+<h2>International Components for Unicode for Java (ICU4J)</h2>
+<h3>Read Me for ICU4J 52</h3>
+(Last Update: 2013-Oct-07)
+<hr size="2" width="100%">
+
+<p><b>Note:</b> This is major release of ICU4J. It contains bug fixes and adds implementations
+of inherited API and introduces new API or functionality.
+</p>
+<p>For the most recent release, see the <a
+ href="http://www.icu-project.org/download/"> ICU4J
+download site</a>. </p>
+<h3 class="doc">Contents</h3>
+<ul type="disc">
+ <li><a href="#introduction">Introduction to ICU4J</a></li>
+ <li><a href="#changes">Changes In This Release</a></li>
+ <li><a href="#license">License Information</a></li>
+ <li><a href="#PlatformDependencies">Platform Dependencies</a></li>
+ <li><a href="#download">How to Download ICU4J</a></li>
+ <li><a href="#WhatContain">The Structure and Contents of ICU4J</a></li>
+ <li><a href="#API">Where to Get Documentation</a></li>
+ <li><a href="#HowToInstallJavac">How to Install and Build</a></li>
+ <li><a href="#HowToModularize">How to modularize ICU4J</a></li>
+ <li><a href="#tryingout">Trying Out ICU4J</a></li>
+ <li><a href="#resources">ICU4J Resource Information</a></li>
+ <li><a href="#timezone">About ICU4J Time Zone</a></li>
+ <li><a href="#WhereToFindMore">Where to Find More Information</a></li>
+ <li><a href="#SubmittingComments">Submitting Comments, Requesting
+Features and Reporting Bugs</a></li>
+</ul>
+<h3 class="doc"><a name="introduction"></a>Introduction to ICU4J</h3>
+<p>The International Components for Unicode (ICU) library provides
+robust and
+full-featured Unicode services on a wide variety of platforms. ICU
+supports the
+most current version of the Unicode standard, including support for
+supplementary characters (needed for GB 18030 repertoire support).</p>
+<p>Java provides a strong foundation for global programs, and IBM and
+the
+ICU team played a key role in providing globalization technology to
+Java. But because of its long release schedule, Java cannot always keep
+up with evolving standards. The ICU team continues to extend Java's
+Unicode and internationalization support, focusing on improving
+performance,
+keeping current with the Unicode standard, and providing richer APIs,
+while
+remaining as compatible as possible with the original Java text and
+internationalization API design.</p>
+<p>ICU4J is an add-on to the regular JRE that provides:
+</p>
+<ul>
+ <li><a
+ href="http://www.icu-project.org/userguide/Collate_Intro.html"><b>Collation</b></a>
+&#8211; rule-based, up-to-date Unicode Collation Algorithm (UCA) sorting order<br>
+&nbsp;&nbsp;&nbsp;&nbsp;For fast multilingual string comparison; faster
+and more complete than
+the J2SE implementation</li>
+ <li><a href="http://www.icu-project.org/userguide/charsetDetection.html"><b>Charset
+Detection</b></a> &#8211; Recognition of various single and multibyte charsets<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Useful for recognizing untagged text data</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/unicodeSet.html"><b>UnicodeSet</b></a>
+&#8211; standard set operations optimized for sets of Unicode characters<br>
+&nbsp;&nbsp;&nbsp;&nbsp;UnicodeSets can be built from string patterns
+using any Unicode properties.</li>
+ <li><a href="http://www.icu-project.org/userguide/Transform.html"><b>Transforms</b></a>
+&#8211; a flexible mechanism for Unicode text conversions<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Including Full/Halfwidth conversions,
+Normalization, Case conversions, Hex
+conversions, and transliterations between scripts (50+ pairs)</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/normalization.html"><b>Unicode
+Normalization</b></a> &#8211; NFC, NFD, NFKD, NFKC<br>
+&nbsp;&nbsp;&nbsp;&nbsp;For canonical text representations, needed for
+XML and the net</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/dateCalendar.html"><b>International
+Calendars</b></a> &#8211; Arabic, Buddhist, Chinese, Hebrew, Japanese, Ethiopic, Islamic, Coptic and other calendars<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Required for correct presentation of dates in
+certain countries</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/formatNumbers.html"><b>Number
+Format
+Enhancements</b></a> &#8211; Scientific Notation, Spelled-out, etc.<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Enhancements to the normal Java number
+formatting. The spell-out format is
+used for checks and similar documents</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/boundaryAnalysis.html"><b>Enhanced
+Word-Break Detection</b></a> &#8211; Rule-based, supports Thai<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Required for correct support of Thai</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/compression.html"><b>Unicode
+Text
+Compression</b></a> &#8211; Standard compression of Unicode text<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Suitable for large numbers of small fields,
+where LZW and similar schemes
+do not apply</li>
+ <li><a
+ href="http://www.icu-project.org/userguide/conversion.html"><b>Charset Conversion</b></a> &#8211; Conversion to and from different charsets.<br>
+&nbsp;&nbsp;&nbsp;&nbsp;Plugs into Java CharsetProvider Service Provider Interface (SPI)</li>
+
+</ul>
+<blockquote>
+ <p><b>Note:</b> We continue to provide assistance to Java, and in some
+cases, ICU4J support has been rolled into a later release of Java. For
+example, BCP47 language tag support including Unicode locale extensions
+is now in Java 7. However, the most current and complete version is always
+found in ICU4J.</p>
+</blockquote>
+
+<h3 class="doc"><a name="changes"></a>Changes In This Release</h3>
+
+<p>See the <a href="http://sites.google.com/site/icusite/download/52">ICU 52 download page</a>
+about new features in this release.
+The list of API changes since the previous ICU4J release is available
+<a href="http://source.icu-project.org/repos/icu/icu4j/tags/release-52-1/APIChangeReport.html">here</a>.</p>
+
+<h3 class="doc"><a name="license"></a>License Information</h3>
+<p>
+The ICU projects (ICU4C and ICU4J) use the X license. The X
+license is <b>suitable for commercial use</b> and is a recommended free software license
+that is compatible with the GNU GPL license. This became
+effective with release 1.8.1 of ICU4C and release 1.3.1 of ICU4J in
+mid-2001. All new ICU releases will adopt the X license; previous ICU
+releases continue to utilize the IPL (IBM Public License). Users
+of previous releases of ICU who want to adopt new ICU releases will
+need to accept the terms and conditions of the X license.
+</p>
+<p>
+The main effect of the change is to provide GPL compatibility.
+The X license is listed as GPL compatible, see the GNU page at
+<a href="http://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses">
+http://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses</a>.
+This means that GPL projects can now use ICU code, it does <b>not</b>
+mean that projects using ICU become subject to GPL.
+</p>
+<p>
+ The IBM version contains the essential text of the license, omitting the
+X-specific trademarks and copyright notices. The full copy of <a
+ href="http://source.icu-project.org/repos/icu/icu4j/tags/release-52-1/main/shared/licenses/license.html">ICU's license</a> is included in the download
+package.
+</p>
+<h3 class="doc"><a name="PlatformDependencies"></a>Platform Dependencies</h3>
+<p>
+ICU4J 52 depends on J2SE 5.0 functionality. Therefore, ICU4J only runs on
+JRE version 5.0 or later.
+The table below shows the operating systems and JRE/VM versions currently
+used by the ICU development team to test ICU4J.
+</p>
+<table border="1">
+<tr>
+ <th rowspan="2">Operating System</th>
+ <th colspan="2">JRE 7</th>
+ <th colspan="2">JRE 6</th>
+ <th colspan="2">JRE 5</th>
+</tr>
+<tr>
+ <th>32bit</th>
+ <th>64bit</th>
+ <th>32bit</th>
+ <th>64bit</th>
+ <th>32bit</th>
+ <th>64bit</th>
+</tr>
+<tr>
+ <th>AIX 6.1</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+ <th>AIX 7.1</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#CCCCFF"><em><b>Reference platform</b></em></td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+ <th>HP-UX 11 (IA64)</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+ <th>Mac OS X 10.6</th>
+ <td align="center">-</td>
+ <td align="center">-</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center">-</td>
+</tr>
+<tr>
+ <th>Redhat Enterprise Linux 6 (x86)</th>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+</tr>
+<tr>
+ <th>Redhat Enterprise Linux 6 (x86_64)</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#CCCCFF"><em><b>Reference platform</b></em></td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+<th>Solaris 10 (SPARC)</th>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+ <th>Solaris 11 (SPARC)</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+<tr>
+ <th>Windows XP</th>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+</tr>
+<tr>
+ <th>Windows Vista</th>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+</tr>
+<tr>
+ <th>Windows 7</th>
+ <td align="center" bgcolor="#CCCCFF"><em><b>Reference platform</b></em></td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+</tr>
+<tr>
+ <th>Windows 2008 Server</th>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+ <td align="center">-</td>
+ <td align="center" bgcolor="#DDDDFF">Regularly tested</td>
+</tr>
+</table>
+
+<h3 class="doc"><a name="download"></a>How to Download ICU4J</h3>
+<p>There are two ways to download the ICU4J releases.
+</p>
+<ul type="disc">
+ <li><b>Official Release:</b><br>
+If you want to use ICU4J (as opposed to developing it), your best bet
+is to download an official, packaged version of the ICU4J library files.
+These versions are tested more thoroughly than day-to-day development
+builds, and they are packaged in jar files for convenient download.
+These packaged files can be found at the
+<a href="http://www.icu-project.org/download/">ICU Download page</a>.
+</li></ul>
+<ul type="disc">
+ <li><b>Subversion Source Repository:</b><br>
+If you are interested in developing features, patches, or bug fixes for
+ICU4J, you should probably be working with the latest version of the
+ICU4J source code. You will need to check the code out of our Subversion
+repository to ensure that you have the most recent version of all of
+the files. There are several ways to do this. Please follow the
+directions that are contained on the <a
+ href="http://www.icu-project.org/repository/">Source
+ Repository page</a> for details.
+ </li>
+</ul>
+<p>For more details on how to download ICU4J directly from the web
+site, please see the ICU download page at <a
+ href="http://www.icu-project.org/download/">http://www.icu-project.org/download/</a>
+</p>
+<h3 class="doc"><a name="WhatContain"></a>The Structure and Contents of
+ICU4J</h3>
+<p>Below, all directory pathes are relative to the directory where the
+ICU4J source archive is extracted.
+</p>
+<p><b>Information and build files:</b></p>
+<table border="1">
+<tr>
+ <th>Path</th>
+ <th>Description</th>
+</tr>
+<tr>
+ <td>readme.html</td>
+ <td>A description of ICU4J (International Components for Unicode for Java)</td>
+</tr>
+<tr>
+ <td>build.html</td>
+ <td>The main Ant build file for ICU4J. See <a href="#HowToInstallJavac">How to Install
+ and Build</a> for more information</td>
+</tr>
+<tr>
+ <td>main/shared/licenses/license.html</td>
+ <td>The X license, used by ICU4J</td>
+</tr>
+</table>
+
+<p><b>ICU4J runtime class files:</b></p>
+<table border="1">
+<tr>
+ <th>Path</th>
+ <th>Sub-component Name</th>
+ <th>Build Dependencies</th>
+ <th>Public API Packages</th>
+ <th>Description</th>
+</tr>
+<tr>
+ <td>main/classes/charset</td>
+ <td>icu4j-charset</td>
+ <td>icu4j-core</td>
+ <td>com.ibm.icu.charset</td>
+ <td>Implementation of <code>java.nio.charset.spi.CharsetProvider</code>.
+ This sub-component is shipped as icu4j-charset.jar along with
+ ICU charset converter data files.</td>
+</tr>
+<tr>
+ <td>main/classes/collate</td>
+ <td>icu4j-collate</td>
+ <td>icu4j-core</td>
+ <td>com.ibm.icu.text<br>
+ com.ibm.icu.util</td>
+ <td>Collator APIs and implementation. Also includes some public API classes
+ that depend on Collator.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/core</td>
+ <td>icu4j-core</td>
+ <td>n/a</td>
+ <td>com.ibm.icu.lang<br>
+ com.ibm.icu.math<br>
+ com.ibm.icu.text<br>
+ com.ibm.icu.util</td>
+ <td>ICU core API classes and implementation.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/currdata</td>
+ <td>icu4j-currdata</td>
+ <td>icu4j-core</td>
+ <td>n/a</td>
+ <td>No public API classes. Provides access to currency display data.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/langdata</td>
+ <td>icu4j-langdata</td>
+ <td>icu4j-core</td>
+ <td>n/a</td>
+ <td>No public API classes. Provides access to language display data.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/localespi</td>
+ <td>icu4j-localespi</td>
+ <td>icu4j-core<br>
+ icu4j-collate<br>
+ </td>
+ <td>n/a</td>
+ <td>Implementation of various locale-sensitive service providers defined
+ in <code>java.text.spi</code> and <code>java.util.spi</code> in J2SE 6.0
+ or later Java releases.
+ This sub-component is shipped as icu4j-localespi.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/regiondata</td>
+ <td>icu4j-regiondata</td>
+ <td>icu4j-core</td>
+ <td>n/a</td>
+ <td>No public API classes. Provides access to region display data.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+<tr>
+ <td>main/classes/translit</td>
+ <td>icu4j-translit</td>
+ <td>icu4j-core</td>
+ <td>com.ibm.icu.text</td>
+ <td>Transliterator APIs and implementation.
+ This sub-component is packaged as a part of icu4j.jar.</td>
+</tr>
+</table>
+
+<p><b>ICU4J unit test files:</b></p>
+<table border="1">
+<tr>
+ <th>Path</th>
+ <th>Sub-component Name</th>
+ <th>Runtime Dependencies</th>
+ <th>Description</th>
+</tr>
+<tr>
+ <td>main/tests/charset</td>
+ <td>icu4j-charset-tests</td>
+ <td>icu4j-charset<br>
+ icu4j-core<br>
+ icu4j-test-framework</td>
+ <td>Test suite for charset sub-component.</td>
+</tr>
+<tr>
+ <td>main/tests/collate</td>
+ <td>icu4j-collate-tests</td>
+ <td>icu4j-collate<br>
+ icu4j-core<br>
+ icu4j-test-framework</td>
+ <td>Test suite for collate sub-component.</td>
+</tr>
+<tr>
+ <td>main/tests/core</td>
+ <td>icu4j-core-tests</td>
+ <td>icu4j-core<br>
+ icu4j-currdata<br>
+ icu4j-langdata<br>
+ icu4j-regiondata<br>
+ icu4j-test-framework</td>
+ <td>Test suite for core sub-component.</td>
+</tr>
+<tr>
+ <td>main/tests/framework</td>
+ <td>icu4j-test-framework</td>
+ <td>icu4j-core</td>
+ <td>Common ICU4J unit test framework and utilities.</td>
+</tr>
+<tr>
+ <td>main/tests/localespi</td>
+ <td>icu4j-localespi-tests</td>
+ <td>icu4j-core<br>
+ icu4j-collate<br>
+ icu4j-currdata<br>
+ icu4j-langdata<br>
+ icu4j-localespi<br>
+ icu4j-regiondata<br>
+ icu4j-test-framework</td>
+ <td>Test suite for localespi sub-component.</td>
+</tr>
+<tr>
+ <td>main/tests/packaging</td>
+ <td>icu4j-packaging-tests</td>
+ <td>icu4j-core<br>
+ icu4j-test-framework</td>
+ <td>Test suite for sub-component packaging.</td>
+</tr>
+<tr>
+ <td>main/tests/translit</td>
+ <td>icu4j-translit-tests</td>
+ <td>icu4j-core<br>
+ icu4j-translit
+ icu4j-test-framework</td>
+ <td>Test suite for translit sub-component.</td>
+</tr>
+</table>
+
+<p><b>Others:</b></p>
+<table border="1">
+<tr>
+ <th>Path</th>
+ <th>Description</th>
+</tr>
+<tr>
+ <td>main/shared</td>
+ <td>Files shared by ICU4J sub-components under the <code>main</code> directory including:
+ <ul>
+ <li>ICU4J runtime data archive (icudata.jar).</li>
+ <li>ICU4J unit test data archive (testdata.jar).</li>
+ <li>Shared Ant build script and configuration files.</li>
+ <li>License files.</li>
+ </ul>
+ </td>
+</tr>
+<tr>
+ <td>demos</td>
+ <td>ICU4J demo programs.</td>
+</tr>
+<tr>
+ <td>perf-tests</td>
+ <td>ICU4J performance test files.</td>
+</tr>
+<tr>
+ <td>tools</td>
+ <td>ICU4J tools including:
+ <ul>
+ <li>Custom JavaDoc taglets used for generating ICU4J API references.</li>
+ <li>API report tool and data.</li>
+ <li>Other independent utilities used for ICU4J development.</li>
+ </ul>
+ </td>
+</tr>
+</table>
+
+<h3 class="doc"><a name="API"></a>Where to get Documentation</h3>
+<p>The <a href="http://www.icu-project.org/userguide/">ICU user's
+guide</a> contains lots of general information about ICU, in its C,
+C++, and Java incarnations.</p>
+<p>The complete API documentation for ICU4J (javadoc) is available on
+the ICU4J web site, and can be built from the sources:
+</p>
+<ul>
+ <li><a href="http://www.icu-project.org/apiref/icu4j/">Index
+to all ICU4J API</a></li>
+ <li><a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/CharsetDetector.html">Charset Detector</a> &#8211; Detection of charset from a byte stream</li>
+ <li>International Calendars &#8211;
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/BuddhistCalendar.html">Buddhist</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/ChineseCalendar.html">Chinese</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/CopticCalendar.html">Coptic</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/EthiopicCalendar.html">Ethiopic</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/GregorianCalendar.html">Gregorian</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/HebrewCalendar.html">Hebrew</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/IndianCalendar.html">Indian</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/IslamicCalendar.html">Islamic</a>,
+ <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/JapaneseCalendar.html">Japanese</a>,
+ Persian, Dangi.</li>
+ <li>Time Zone Enhancements &#8211;
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/BasicTimeZone.html">Time zone transition and rule detection</a>,
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/VTimeZone.html">iCalendar VTIMEZONE formatting and parsing</a>,
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/RuleBasedTimeZone.html">Custom time zones constructed by user defined rules</a>.
+ <li>Date Format Enhancements &#8211; <a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/DateTimePatternGenerator.html">Date/Time Pattern Generator</a>,
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/DateIntervalFormat.html">Date Interval Format</a>,
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/DurationFormat.html">Duration Format</a>.
+ <li><a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/Normalizer.html">Unicode
+Normalization</a> &#8211; Canonical text representation for W3C.</li>
+ <li><a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/NumberFormat.html">Number
+Format Enhancements</a> &#8211; Scientific Notation, Spelled out.</li>
+ <li><a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/BreakIterator.html">Enhanced
+word-break detection</a> &#8211; Rule-based, supports Thai</li>
+ <li><a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/Transliterator.html">Transliteration</a>
+&#8211; A general framework for converting text from one format to another,
+e.g. Cyrillic to Latin, or Hex to Unicode. </li>
+ <li>Unicode Text <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/UnicodeCompressor.html">Compression</a>
+&amp; <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/UnicodeDecompressor.html">Decompression</a>
+&#8211; 2:1 compression on English Unicode text.</li>
+ <li>Collation &#8211; <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/RuleBasedCollator.html">Rule-based
+sorting</a>, <a
+ href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/StringSearch.html">Efficient
+multi-lingual searching</a>,
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/AlphabeticIndex.html">Alphabetic indexing</a></li>
+</ul>
+<h3 class="doc"><a name="HowToInstallJavac"></a>How to Install and Build</h3>
+<p>
+To install ICU4J, simply place the prebuilt jar file <strong>icu4j.jar</strong>
+on your Java CLASSPATH. If you need Charset API support please also place
+<strong>icu4j-charset.jar</strong> on your class path along with <strong>icu4j.jar</strong>.
+</p>
+<p>
+To build ICU4J, you will need J2SE SDK 5.0 or later (ICU4J locale SPI
+provider sub-components require J2SE SDK 6.0 or later) and the Ant build system
+version 1.7 or later. It's recommended to install both the J2SE SDK and Ant
+somewhere <em>outside</em>the ICU4J directory. For example, on Linux you might install
+these in <code>/usr/local</code>.</p>
+<ul>
+ <li>Install J2SE SDK 7.</li>
+ <li>Install the <a href="http://ant.apache.org/"><strong>Ant</strong></a>
+ build system. Ant is a portable, Java-based build system similar to
+ make. ICU4J uses Ant because it introduces no other dependencies, it's
+ portable, and it's easier to manage than a collection of makefiles. We
+ currently build ICU4J using a single makefile on all platforms Ant.
+ The build system requires Ant 1.7 or later.
+ <p>Installing Ant is straightforward. Download it (see <a
+ href="http://ant.apache.org/bindownload.cgi">http://ant.apache.org/bindownload.cgi</a>),
+extract it onto your system, set some environment variables, and add
+its bin directory to your path. For example: </p>
+ <pre>
+ set JAVA_HOME=C:\jdk1.7.0
+ set ANT_HOME=C:\ant
+ set PATH=%JAVA_HOME%\bin;%ANT_HOME%\bin;%PATH%</pre>
+ <p>See the current Ant documentation for details.</p>
+ </li>
+</ul>
+<p>Once the J2SE SDK and Ant are installed, building is just a matter of
+typing <strong>ant</strong> in the ICU4J root directory. This causes the
+Ant build system to perform the build target <strong>jar</strong>
+as specified by the file <strong>build.xml</strong>, located in the ICU4J
+root directory. You can give Ant options like -verbose, and you can specify
+other targets. For example:</p>
+<blockquote>
+<pre>C:\icu4j>ant
+Buildfile: C:\icu4j\build.xml
+
+info:
+ [echo] ----- Build Environment Information -------------------
+ [echo] Java Home: C:\jdk1.7.0\jre
+ [echo] Java Version: 1.7.0
+ [echo] Ant Home: C:\ant
+ [echo] Ant Version: Apache Ant(TM) version 1.9.2 compiled on July 8 2013
+ [echo] OS: Windows 7
+ [echo] OS Version: 6.1
+ [echo] OS Arch: amd64
+ [echo] Host: ICUDEV
+ [echo] -------------------------------------------------------
+
+core:
+
+@compile:
+ [echo] --- java compiler arguments ------------------------
+ [echo] source dir: C:\icu4j\main\classes\core/src
+ [echo] output dir: C:\icu4j\main\classes\core/out/bin
+ [echo] classpath:
+ [echo] source: 1.5
+ [echo] target: 1.5
+ [echo] debug: on
+ [echo] encoding: UTF-8
+ [echo] compiler arg: -Xlint:all,-deprecation,-dep-ann,-options
+ [echo] ----------------------------------------------------
+ [mkdir] Created dir: C:\icu4j\main\classes\core\out\bin
+ [javac] Compiling 353 source files to C:\icu4j\main\classes\core\out\bin
+ [javac] Note: Some input files use or override a deprecated API.
+ [javac] Note: Recompile with -Xlint:deprecation for details.
+
+compile:
+
+@copy:
+ [copy] Copying 23 files to C:\icu4j\main\classes\core\out\bin
+
+copy-data:
+ [unjar] Expanding: C:\icu4j\main\shared\data\icudata.jar into C:\icu4j\main\
+classes\core\out\bin
+ [unjar] Expanding: C:\icu4j\main\shared\data\icutzdata.jar into C:\icu4j\mai
+n\classes\core\out\bin
+
+...
+...
+...
+
+_build-localespi:
+
+@compile:
+ [echo] --- java compiler arguments ------------------------
+ [echo] source dir: C:\icu4j\main\classes\localespi/src
+ [echo] output dir: C:\icu4j\main\classes\localespi/out/bin
+ [echo] classpath: C:\icu4j\main\classes\core\out\lib\icu4j-core.jar;C:
+\icu4j\main\classes\collate\out\lib\icu4j-collate.jar
+ [echo] source: 1.6
+ [echo] target: 1.6
+ [echo] debug: on
+ [echo] encoding: UTF-8
+ [echo] compiler arg: -Xlint:all,-deprecation,-dep-ann,-options
+ [echo] ----------------------------------------------------
+ [mkdir] Created dir: C:\icu4j\main\classes\localespi\out\bin
+ [javac] Compiling 22 source files to C:\icu4j\main\classes\localespi\out\bin
+
+
+compile:
+
+@copy:
+ [copy] Copying 10 files to C:\icu4j\main\classes\localespi\out\bin
+
+copy:
+
+@jar:
+ [mkdir] Created dir: C:\icu4j\main\classes\localespi\out\lib
+ [copy] Copying 1 file to C:\icu4j\main\classes\localespi\out
+ [jar] Building jar: C:\icu4j\main\classes\localespi\out\lib\icu4j-localesp
+i.jar
+
+jar:
+
+@src-jar:
+ [jar] Building jar: C:\icu4j\main\classes\localespi\out\lib\icu4j-localesp
+i-src.jar
+
+src-jar:
+
+build:
+
+jar:
+ [copy] Copying 1 file to C:\icu4j
+ [copy] Copying 1 file to C:\icu4j
+
+BUILD SUCCESSFUL
+Total time: 1 minute 51 seconds</pre>
+</blockquote>
+<I>Note: The above output is an example. The numbers are likely to be different with the current version ICU4J.</I>
+<p>The following are some targets that you can provide to <b>ant</b>.
+For more targets run <code>ant -projecthelp</code> or see the build.xml file.</p>
+<table border="1">
+<tr>
+ <th>jar (default)</th>
+ <td>Create ICU4J runtime library jar archives (<code>icu4j.jar</code>,
+ <code>icu4j-charset.jar</code> and <code>icu4j-localespi.jar</code>)
+ in the root ICU4J directory.</td>
+</tr>
+<tr>
+ <th>check</th>
+ <td>Build all ICU4J runtime library classes and corresponding unit test cases,
+ then run the tests.</td>
+</tr>
+<tr>
+ <th>clean</th>
+ <td>Remove all build output files.</td>
+</tr>
+<tr>
+ <th>main</th>
+ <td>Build all ICU4J runtime library sub-components (under the directory
+ <code>main/classes</code>).</td>
+</tr>
+<tr>
+ <th>tests</th>
+ <td>Build all ICU4J unit test sub-components (under the directory <code>main/tests</code>)
+ and their dependencies.</td>
+</tr>
+<tr>
+ <th>tools</th>
+ <td>Build the tools.</td>
+</tr>
+<tr>
+ <th>docs</th>
+ <td>Run javadoc over the ICU4J runtime library files, generating an HTML documentation
+ tree in the subdirectory <code>doc</code>.</td>
+</tr>
+<tr>
+ <th>jarDocs</th>
+ <td>Create ICU4J doc jar archive (<code>icu4jdocs.jar</code>) containing API reference
+ docs in the root ICU4J directory. </td>
+</tr>
+<tr>
+ <th>jarDemos</th>
+ <td>Create ICU4J demo jar archive (<code>icu4jdemos.jar</code>) in the root ICU4J
+ directory.</td>
+</tr>
+</table>
+
+<p>For more information, read the Ant documentation and the <strong>build.xml</strong>
+file.</p>
+<p><b>Note:</b> If you get an OutOfMemoryError when you are running <tt>"ant check"</tt>,
+you can set the heap size of the jvm by setting the environment variable JVM_OPTIONS
+to the appropriate java options.</p>
+
+<p><b>Eclipse users:</b> See the ICU4J site for information on<a
+ href="http://www.icu-project.org/docs/eclipse_howto/eclipse_howto.html">
+how to configure Eclipse</a> to build and develop ICU4J on Eclipse IDE.</p>
+
+<p><b>Note: </b>To install and configure ICU4J Locale Service Provider, please refer the user guide
+page <a href="http://userguide.icu-project.org/icu4j-locale-service-provider">ICU4J Locale
+Service Provider</a>.</p>
+
+<h3 class="doc"><a name="HowToModularize"></a>How to modularize ICU4J</h3>
+<p>Some clients may not wish to ship all of ICU4J with their
+application, since the application might only use a small part of ICU4J.
+ICU4J release 2.6 and later provide build options to build individual
+ICU4J 'modules' for a more compact distribution. For more details, please
+refer to the section <em>Modularization of ICU4J</em> in the ICU user's
+guide article <a href="http://userguide.icu-project.org/packaging-icu4j">Packaging ICU4J</a>.
+
+<h3 class="doc"><a name="tryingout"></a>Trying Out ICU4J</h3>
+<p><strong>Note:</strong> the demos provided with ICU4J are for the
+most part undocumented. This list can show you where to look, but
+you'll have to experiment a bit. The demos are <strong>unsupported</strong>
+and may change or disappear without notice.</p>
+<p>The icu4j.jar file contains only the ICU4J runtime library classes, not the
+demo classes, so unless you build ICU4J there is little to try out.
+</p>
+<h4>Charset</h4>
+To try out the <strong>Charset</strong> package, build <strong>icu4j.jar</strong> and
+<strong>icu4j-charset.jar</strong> using the 'jar' target.
+You can use the charsets by placing these files on your classpath.
+<blockquote><tt>java -cp $icu4j_root/icu4j.jar:$icu4j_root/icu4j-charset.jar &lt;your program&gt;</tt></blockquote>
+<h4>Other demos</h4>
+<p>The other demo programs are <strong>not supported</strong> and
+exist only to let you experiment with the ICU4J classes. First, build ICU4J using <tt>ant&nbsp;jarDemos</tt>.
+Then launch the demos as below:</p>
+<blockquote><tt>java -jar $icu4j_root/icu4jdemos.jar</tt></blockquote>
+
+<h3 class="doc"><a name="resources">ICU4J Resource Information</a></h3>
+Starting with release 2.1, ICU4J includes its own
+resource information
+which is completely independent of the JRE resource information. (Note,
+ICU4J 2.8 to 3.4, time zone information depends on the underlying JRE).
+The ICU4J resource information is equivalent to the information in ICU4C and
+many resources are, in fact, the same binary files that ICU4C uses.
+<p>
+By default the ICU4J distribution includes all of the standard resource
+information. It is located under the directory com/ibm/icu/impl/data.
+Depending on the service, the data is in different locations and in
+different formats. <strong>Note:</strong> This will continue to change
+from release to release, so clients should not depend on the exact
+organization
+of the data in ICU4J.</p>
+<ul>
+ <li>The primary <b>locale data</b> is under the directory <tt>icudt52b</tt>,
+ as a set of <tt>".res"</tt> files whose names are the locale identifiers.
+ Locale naming is documented the <code>com.ibm.icu.util.ULocale</code>
+ class, and the use of these names in searching for resources is documented
+ in <code>com.ibm.icu.util.UResourceBundle</code>.</li>
+
+ <li>The <b>collation data</b> is under the directory <tt>icudt52b/coll</tt>,
+ as a set of <tt>".res"</tt> files.</li>
+
+ <li>The <b>currency display name data</b> is under the directory <tt>icudt52b/curr</tt>,
+ as a set of <tt>".res"</tt> files.</li>
+
+ <li>The <b>language display name data</b> is under the directory <tt>icudt52b/lang</tt>,
+ as a set of <tt>".res"</tt> files.</li>
+
+ <li>The <b>region display name data</b> is under the directory <tt>icudt52b/region</tt>,
+ as a set of <tt>".res"</tt> files.</li>
+
+ <li>The <b>rule-based transliterator data</b> is under the directory
+ <tt>icudt52b/translit</tt>, as a set of <tt>".res"</tt> files.</li>
+
+ <li>The <b>rule-based number format data</b> is under the directory
+ <tt>icudt52b/rbnf</tt>, as a set of <tt>".res"</tt> files.
+
+ <li>The <b>break iterator data</b> is directly under the
+ directory <tt>icudt52b</tt>, as a set of <tt>".brk"</tt> files, named according to the
+ type of break and the locale where there are locale-specific versions.</li>
+
+ <li>The <b>holiday data</b> is under the directory <tt>icudt52b</tt>,
+ as a set of <tt>".class"</tt> files, named <tt>"HolidayBundle_"</tt>
+ followed by the locale ID.</li>
+
+ <li>The <b>character property data</b> and default <b>unicode collation algorithm
+ (UCA) data</b> is found under the directory <tt>icudt52b</tt>, as a set of
+ <tt>".icu"</tt> files. </li>
+
+ <li>The <b>normalization data</b> is found under the directory <tt>icudt52b</tt>,
+ as a set of <tt>".nrm"</tt> files. </li>
+
+ <li>The <b>character set converter data</b> is under the directory
+ <tt>icudt52b</tt>, as a set of <tt>".cnv"</tt> files. These files are
+ currently included only in icu-charset.jar.</li>
+
+ <li>The <b>time zone rule data</b> is under the directory
+ <tt>icudt52b</tt>, as <tt>zoneinfo64.res</tt>.</li>
+
+ <li>The <b>time zone display name data</b> is under the directory
+ <tt>icudt52b/zone</tt>, as a set of <tt>".res"</tt> files.</li>
+</ul>
+<p>
+Some of the data files alias or otherwise reference data from other
+data files. One reason for this is because some locale names have
+changed. For example, <tt>he_IL</tt> used to be <tt>iw_IL</tt>. In
+order to support both names but not duplicate the data, one of the
+resource files refers to the other file's data. In other cases, a
+file may alias a portion of another file's data in order to save
+space. Currently ICU4J provides no tool for revealing these
+dependencies.</p>
+<blockquote><strong>Note:</strong> Java's <code>Locale</code> class
+silently converts the language code <tt>"he"</tt> to <tt>"iw"</tt>
+when you construct the Locale (for versions of Java through Java 5). Thus
+Java cannot be used to locate resources that use the <tt>"he"</tt>
+language code. ICU, on the other hand, does not perform this
+conversion in ULocale, and instead uses aliasing in the locale data to
+represent the same set of data under different locale
+ids.</blockquote>
+<p>
+Resource files that use locale ids form a hierarchy, with up to four
+levels: a root, language, region (country), and variant. Searches for
+locale data attempt to match as far down the hierarchy as possible,
+for example, <tt>"he_IL"</tt> will match <tt>he_IL</tt>, but
+<tt>"he_US"</tt> will match <tt>he</tt> (since there is no <tt>US</tt>
+variant for he, and <tt>"xx_YY</tt> will match root (the
+default fallback locale) since there is no <tt>xx</tt> language code
+in the locale hierarchy. Again, see
+<code>java.util.ResourceBundle</code> for more information.
+</p>
+<p>
+<strong>Currently ICU4J provides no tool for revealing these
+dependencies</strong> between data files, so trimming the data
+directly in the ICU4J project is a hit-or-miss affair. The key point
+when you remove data is to make sure to remove all dependencies on
+that data as well. For example, if you remove <tt>he.res</tt>, you
+need to remove <tt>he_IL.res</tt>, since it is lower in the hierarchy,
+and you must remove iw.res, since it references <tt>he.res</tt>, and
+<tt>iw_IL.res</tt>, since it depends on it (and also references
+<tt>he_IL.res</tt>).
+</p>
+<p>
+Unfortunately, the jar tool in the JDK provides no way to remove items
+from a jar file. Thus you have to extract the resources, remove the
+ones you don't want, and then create a new jar file with the remining
+resources. See the jar tool information for how to do this. Before
+'rejaring' the files, be sure to thoroughly test your application with
+the remaining resources, making sure each required resource is
+present.
+</p>
+<h4>Using additional resource files with ICU4J</h4>
+<blockquote>
+ <table cellpadding="3" frame="border" rules="none" width="50%">
+ <tbody>
+ <tr>
+ <td><b><font color="red" size="+1">Warning:</font> Resource
+file formats can change across releases of ICU4J!</b></td>
+ </tr>
+ <tr>
+ <td>The format of ICU4J resources is not part of the API.
+Clients who develop their own resources for use with ICU4J should be
+prepared to
+regenerate them when they move to new releases of ICU4J.</td>
+ </tr>
+ </tbody>
+ </table>
+</blockquote>
+<p>
+We are still developing ICU4J's resource mechanism. Currently it
+is not possible to mix icu's new binary <tt>.res</tt>
+resources
+with traditional java-style <tt>.class</tt> or <tt>.txt</tt>
+resources. We might
+allow for this in a future release, but since the resource data and
+format is not formally
+supported, you run the risk of incompatibilities with future releases
+of ICU4J.
+</p>
+<p>
+Resource data in ICU4J is checked in to the repository as a jar file
+containing the resource binaries, <tt>$icu4j_root/main/shared/data/icudata.jar</tt>.
+This means that inspecting the contents of these resources is difficult.
+They currently are compiled from ICU4C <tt>.txt</tt> file data. You
+can view the contents of the ICU4C text resource files to understand
+the contents of the ICU4J resources.
+</p>
+<p>
+The files in <tt>icudata.jar</tt> get extracted to <tt>com/ibm/icu/impl/data</tt>
+in the build output directory by some build targets.
+</p>
+<h4><a name="resourcesICU4C">Building ICU4J Resources from ICU4C</a></h4>
+ICU4J data is built by ICU4C tools. Please see "icu4j-readme.txt" in <I>$icu4c_root</I>/source/data for the procedures.
+<h5> Generating Data from CLDR </h5>
+<I> Note: This procedure assumes that all 3 sources are present</I>
+<ol>
+ <li>Checkout or download CLDR version 'release-24'</li>
+ <li>Checkout ICU4C with tag 'release-52-1'</li>
+ <li>Checkout ICU4J with tag 'release-52-1'</li>
+ <li>cd to <I>$icu4c_root</I>/source/data directory</li>
+ <li>Follow the instructions in <I>$icu4c_root</I>/source/data/cldr-icu-readme.txt</li>
+ <li>Rebuild ICU4C with the newly generated data.</li>
+ <li>Run ICU4C tests to verify that the new data is good.</li>
+ <li>Build ICU4J data from ICU4C data by following the procedures in <I>$icu4c_root</I>/source/data/icu4j-readme.txt</li>
+ <li>cd to <I>$icu4j_root</I> dir</li>
+
+ <li>Build and test icu4j</li>
+</ol>
+
+<h3 class="doc"><a name="timezone"></a>About ICU4J Time Zone</h3>
+<p>ICU4J 52.1 includes time zone data version 2013g, which is the latest one as of
+the release date. However, time zone data is frequently updated in response
+to changes made by local governments around the world. If you need to update
+the time zone data, please refer the ICU user guide topic
+<a href="http://userguide.icu-project.org/datetime/timezone#TOC-Updating-the-Time-Zone-Data">Updating the Time Zone Data</a>.</p>
+<p>Starting with ICU4J 4.0, you can optionally configure ICU4J date and time
+service classes to use underlying JDK TimeZone implementation (see the ICU4J API reference
+<a href="http://www.icu-project.org/apiref/icu4j/com/ibm/icu/util/TimeZone.html">TimeZone</a>
+for the details). When this configuration is enabled, ICU's own time zone data
+won't be used and you have to get time zone data patches from the JRE vendor.</p>
+
+<h3 class="doc"><a name="WhereToFindMore"></a>Where to Find More
+Information</h3>
+<p><a href="http://www.icu-project.org/">http://www.icu-project.org/</a>
+is the home page of International Components for Unicode development project</p>
+<p><a href="http://www.ibm.com/software/globalization/icu/">http://www.ibm.com/software/globalization/icu/</a>
+is a pointer to general information about the International Components for
+Unicode hosted by IBM</p>
+<p><a href="http://www.ibm.com/software/globalization/">http://www.ibm.com/software/globalization/</a>
+is a pointer to
+information on how to make applications global. </p>
+<h3 class="doc"><a name="SubmittingComments"></a>Submitting Comments,
+Requesting Features and
+Reporting Bugs</h3>
+<p>Your comments are important to making ICU4J successful. We are
+committed to investigate any bug reports or suggestions,
+and will use your feedback to help plan future releases.</p>
+<p>To submit comments, request features and report bugs,
+please see <a href="http://www.icu-project.org/bugs.html">ICU bug database
+information</a> or contact us through the <a
+ href="http://www.icu-project.org/contacts.html">ICU Support
+mailing list</a>. While we are not able to respond individually to each comment, we do
+review all comments.</p>
+<br>
+<br>
+<h2>Thank you for your interest in ICU4J!</h2>
+<br>
+<hr align="center" size="2" width="100%">
+<p><I><font size="-1">Copyright &copy; 2002-2013 International Business
+Machines Corporation and others. All Rights
+Reserved.<br>
+4400 North First Street, San Jos&eacute;, CA 95193, USA
+</font></I></p>
+</body>
+</html>
diff --git a/src/main/com/android/i18n/MessageFormat.java b/src/main/com/android/i18n/MessageFormat.java
new file mode 100644
index 0000000..98510fc
--- /dev/null
+++ b/src/main/com/android/i18n/MessageFormat.java
@@ -0,0 +1,2673 @@
+/*
+**********************************************************************
+* Copyright (c) 2004-2013, International Business Machines
+* Corporation and others. All Rights Reserved.
+**********************************************************************
+* Author: Alan Liu
+* Created: April 6, 2004
+* Since: ICU 3.0
+**********************************************************************
+*/
+package com.ibm.icu.text;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedCharacterIterator.Attribute;
+import java.text.AttributedString;
+import java.text.CharacterIterator;
+import java.text.ChoiceFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.ibm.icu.impl.PatternProps;
+import com.ibm.icu.impl.Utility;
+import com.ibm.icu.text.MessagePattern.ArgType;
+import com.ibm.icu.text.MessagePattern.Part;
+import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.text.PluralRules.PluralType;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.ULocale.Category;
+
+/**
+ * {@icuenhanced java.text.MessageFormat}.{@icu _usage_}
+ *
+ * <p>MessageFormat prepares strings for display to users,
+ * with optional arguments (variables/placeholders).
+ * The arguments can occur in any order, which is necessary for translation
+ * into languages with different grammars.
+ *
+ * <p>A MessageFormat is constructed from a <em>pattern</em> string
+ * with arguments in {curly braces} which will be replaced by formatted values.
+ *
+ * <p><code>MessageFormat</code> differs from the other <code>Format</code>
+ * classes in that you create a <code>MessageFormat</code> object with one
+ * of its constructors (not with a <code>getInstance</code> style factory
+ * method). Factory methods aren't necessary because <code>MessageFormat</code>
+ * itself doesn't implement locale-specific behavior. Any locale-specific
+ * behavior is defined by the pattern that you provide and the
+ * subformats used for inserted arguments.
+ *
+ * <p>Arguments can be named (using identifiers) or numbered (using small ASCII-digit integers).
+ * Some of the API methods work only with argument numbers and throw an exception
+ * if the pattern has named arguments (see {@link #usesNamedArguments()}).
+ *
+ * <p>An argument might not specify any format type. In this case,
+ * a Number value is formatted with a default (for the locale) NumberFormat,
+ * a Date value is formatted with a default (for the locale) DateFormat,
+ * and for any other value its toString() value is used.
+ *
+ * <p>An argument might specify a "simple" type for which the specified
+ * Format object is created, cached and used.
+ *
+ * <p>An argument might have a "complex" type with nested MessageFormat sub-patterns.
+ * During formatting, one of these sub-messages is selected according to the argument value
+ * and recursively formatted.
+ *
+ * <p>After construction, a custom Format object can be set for
+ * a top-level argument, overriding the default formatting and parsing behavior
+ * for that argument.
+ * However, custom formatting can be achieved more simply by writing
+ * a typeless argument in the pattern string
+ * and supplying it with a preformatted string value.
+ *
+ * <p>When formatting, MessageFormat takes a collection of argument values
+ * and writes an output string.
+ * The argument values may be passed as an array
+ * (when the pattern contains only numbered arguments)
+ * or as a Map (which works for both named and numbered arguments).
+ *
+ * <p>Each argument is matched with one of the input values by array index or map key
+ * and formatted according to its pattern specification
+ * (or using a custom Format object if one was set).
+ * A numbered pattern argument is matched with a map key that contains that number
+ * as an ASCII-decimal-digit string (without leading zero).
+ *
+ * <h4><a name="patterns">Patterns and Their Interpretation</a></h4>
+ *
+ * <code>MessageFormat</code> uses patterns of the following form:
+ * <blockquote><pre>
+ * message = messageText (argument messageText)*
+ * argument = noneArg | simpleArg | complexArg
+ * complexArg = choiceArg | pluralArg | selectArg | selectordinalArg
+ *
+ * noneArg = '{' argNameOrNumber '}'
+ * simpleArg = '{' argNameOrNumber ',' argType [',' argStyle] '}'
+ * choiceArg = '{' argNameOrNumber ',' "choice" ',' choiceStyle '}'
+ * pluralArg = '{' argNameOrNumber ',' "plural" ',' pluralStyle '}'
+ * selectArg = '{' argNameOrNumber ',' "select" ',' selectStyle '}'
+ * selectordinalArg = '{' argNameOrNumber ',' "selectordinal" ',' pluralStyle '}'
+ *
+ * choiceStyle: see {@link ChoiceFormat}
+ * pluralStyle: see {@link PluralFormat}
+ * selectStyle: see {@link SelectFormat}
+ *
+ * argNameOrNumber = argName | argNumber
+ * argName = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
+ * argNumber = '0' | ('1'..'9' ('0'..'9')*)
+ *
+ * argType = "number" | "date" | "time" | "spellout" | "ordinal" | "duration"
+ * argStyle = "short" | "medium" | "long" | "full" | "integer" | "currency" | "percent" | argStyleText
+ * </pre></blockquote>
+ *
+ * <ul>
+ * <li>messageText can contain quoted literal strings including syntax characters.
+ * A quoted literal string begins with an ASCII apostrophe and a syntax character
+ * (usually a {curly brace}) and continues until the next single apostrophe.
+ * A double ASCII apostrohpe inside or outside of a quoted string represents
+ * one literal apostrophe.
+ * <li>Quotable syntax characters are the {curly braces} in all messageText parts,
+ * plus the '#' sign in a messageText immediately inside a pluralStyle,
+ * and the '|' symbol in a messageText immediately inside a choiceStyle.
+ * <li>See also {@link MessagePattern.ApostropheMode}
+ * <li>In argStyleText, every single ASCII apostrophe begins and ends quoted literal text,
+ * and unquoted {curly braces} must occur in matched pairs.
+ * </ul>
+ *
+ * <p>Recommendation: Use the real apostrophe (single quote) character \u2019 for
+ * human-readable text, and use the ASCII apostrophe (\u0027 ' )
+ * only in program syntax, like quoting in MessageFormat.
+ * See the annotations for U+0027 Apostrophe in The Unicode Standard.
+ *
+ * <p>The <code>choice</code> argument type is deprecated.
+ * Use <code>plural</code> arguments for proper plural selection,
+ * and <code>select</code> arguments for simple selection among a fixed set of choices.
+ *
+ * <p>The <code>argType</code> and <code>argStyle</code> values are used to create
+ * a <code>Format</code> instance for the format element. The following
+ * table shows how the values map to Format instances. Combinations not
+ * shown in the table are illegal. Any <code>argStyleText</code> must
+ * be a valid pattern string for the Format subclass used.
+ *
+ * <p><table border=1>
+ * <tr>
+ * <th>argType
+ * <th>argStyle
+ * <th>resulting Format object
+ * <tr>
+ * <td colspan=2><i>(none)</i>
+ * <td><code>null</code>
+ * <tr>
+ * <td rowspan=5><code>number</code>
+ * <td><i>(none)</i>
+ * <td><code>NumberFormat.getInstance(getLocale())</code>
+ * <tr>
+ * <td><code>integer</code>
+ * <td><code>NumberFormat.getIntegerInstance(getLocale())</code>
+ * <tr>
+ * <td><code>currency</code>
+ * <td><code>NumberFormat.getCurrencyInstance(getLocale())</code>
+ * <tr>
+ * <td><code>percent</code>
+ * <td><code>NumberFormat.getPercentInstance(getLocale())</code>
+ * <tr>
+ * <td><i>argStyleText</i>
+ * <td><code>new DecimalFormat(argStyleText, new DecimalFormatSymbols(getLocale()))</code>
+ * <tr>
+ * <td rowspan=6><code>date</code>
+ * <td><i>(none)</i>
+ * <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
+ * <tr>
+ * <td><code>short</code>
+ * <td><code>DateFormat.getDateInstance(DateFormat.SHORT, getLocale())</code>
+ * <tr>
+ * <td><code>medium</code>
+ * <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
+ * <tr>
+ * <td><code>long</code>
+ * <td><code>DateFormat.getDateInstance(DateFormat.LONG, getLocale())</code>
+ * <tr>
+ * <td><code>full</code>
+ * <td><code>DateFormat.getDateInstance(DateFormat.FULL, getLocale())</code>
+ * <tr>
+ * <td><i>argStyleText</i>
+ * <td><code>new SimpleDateFormat(argStyleText, getLocale())
+ * <tr>
+ * <td rowspan=6><code>time</code>
+ * <td><i>(none)</i>
+ * <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
+ * <tr>
+ * <td><code>short</code>
+ * <td><code>DateFormat.getTimeInstance(DateFormat.SHORT, getLocale())</code>
+ * <tr>
+ * <td><code>medium</code>
+ * <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
+ * <tr>
+ * <td><code>long</code>
+ * <td><code>DateFormat.getTimeInstance(DateFormat.LONG, getLocale())</code>
+ * <tr>
+ * <td><code>full</code>
+ * <td><code>DateFormat.getTimeInstance(DateFormat.FULL, getLocale())</code>
+ * <tr>
+ * <td><i>argStyleText</i>
+ * <td><code>new SimpleDateFormat(argStyleText, getLocale())
+ * <tr>
+ * <td><code>spellout</code>
+ * <td><i>argStyleText (optional)</i>
+ * <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.SPELLOUT)
+ * <br/>&nbsp;&nbsp;&nbsp;&nbsp;.setDefaultRuleset(argStyleText);</code>
+ * <tr>
+ * <td><code>ordinal</code>
+ * <td><i>argStyleText (optional)</i>
+ * <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.ORDINAL)
+ * <br/>&nbsp;&nbsp;&nbsp;&nbsp;.setDefaultRuleset(argStyleText);</code>
+ * <tr>
+ * <td><code>duration</code>
+ * <td><i>argStyleText (optional)</i>
+ * <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.DURATION)
+ * <br/>&nbsp;&nbsp;&nbsp;&nbsp;.setDefaultRuleset(argStyleText);</code>
+ * </table>
+ * <p>
+ *
+ * <h4><a name="diffsjdk">Differences from java.text.MessageFormat</a></h4>
+ *
+ * <p>The ICU MessageFormat supports both named and numbered arguments,
+ * while the JDK MessageFormat only supports numbered arguments.
+ * Named arguments make patterns more readable.
+ *
+ * <p>ICU implements a more user-friendly apostrophe quoting syntax.
+ * In message text, an apostrophe only begins quoting literal text
+ * if it immediately precedes a syntax character (mostly {curly braces}).<br>
+ * In the JDK MessageFormat, an apostrophe always begins quoting,
+ * which requires common text like "don't" and "aujourd'hui"
+ * to be written with doubled apostrophes like "don''t" and "aujourd''hui".
+ * For more details see {@link MessagePattern.ApostropheMode}.
+ *
+ * <p>ICU does not create a ChoiceFormat object for a choiceArg, pluralArg or selectArg
+ * but rather handles such arguments itself.
+ * The JDK MessageFormat does create and use a ChoiceFormat object
+ * (<code>new ChoiceFormat(argStyleText)</code>).
+ * The JDK does not support plural and select arguments at all.
+ *
+ * <h4>Usage Information</h4>
+ *
+ * <p>Here are some examples of usage:
+ * <blockquote>
+ * <pre>
+ * Object[] arguments = {
+ * 7,
+ * new Date(System.currentTimeMillis()),
+ * "a disturbance in the Force"
+ * };
+ *
+ * String result = MessageFormat.format(
+ * "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
+ * arguments);
+ *
+ * <em>output</em>: At 12:30 PM on Jul 3, 2053, there was a disturbance
+ * in the Force on planet 7.
+ *
+ * </pre>
+ * </blockquote>
+ * Typically, the message format will come from resources, and the
+ * arguments will be dynamically set at runtime.
+ *
+ * <p>Example 2:
+ * <blockquote>
+ * <pre>
+ * Object[] testArgs = { 3, "MyDisk" };
+ *
+ * MessageFormat form = new MessageFormat(
+ * "The disk \"{1}\" contains {0} file(s).");
+ *
+ * System.out.println(form.format(testArgs));
+ *
+ * // output, with different testArgs
+ * <em>output</em>: The disk "MyDisk" contains 0 file(s).
+ * <em>output</em>: The disk "MyDisk" contains 1 file(s).
+ * <em>output</em>: The disk "MyDisk" contains 1,273 file(s).
+ * </pre>
+ * </blockquote>
+ *
+ * <p>For messages that include plural forms, you can use a plural argument:
+ * <pre>
+ * MessageFormat msgFmt = new MessageFormat(
+ * "{num_files, plural, " +
+ * "=0{There are no files on disk \"{disk_name}\".}" +
+ * "=1{There is one file on disk \"{disk_name}\".}" +
+ * "other{There are # files on disk \"{disk_name}\".}}",
+ * ULocale.ENGLISH);
+ * Map args = new HashMap();
+ * args.put("num_files", 0);
+ * args.put("disk_name", "MyDisk");
+ * System.out.println(msgFmt.format(args));
+ * args.put("num_files", 3);
+ * System.out.println(msgFmt.format(args));
+ *
+ * <em>output</em>:
+ * There are no files on disk "MyDisk".
+ * There are 3 files on "MyDisk".
+ * </pre>
+ * See {@link PluralFormat} and {@link PluralRules} for details.
+ *
+ * <h4><a name="synchronization">Synchronization</a></h4>
+ *
+ * <p>MessageFormats are not synchronized.
+ * It is recommended to create separate format instances for each thread.
+ * If multiple threads access a format concurrently, it must be synchronized
+ * externally.
+ *
+ * @see java.util.Locale
+ * @see Format
+ * @see NumberFormat
+ * @see DecimalFormat
+ * @see ChoiceFormat
+ * @see PluralFormat
+ * @see SelectFormat
+ * @author Mark Davis
+ * @author Markus Scherer
+ * @stable ICU 3.0
+ */
+public class MessageFormat extends UFormat {
+
+ // Incremented by 1 for ICU 4.8's new format.
+ static final long serialVersionUID = 7136212545847378652L;
+
+ /**
+ * Constructs a MessageFormat for the default <code>FORMAT</code> locale and the
+ * specified pattern.
+ * Sets the locale and calls applyPattern(pattern).
+ *
+ * @param pattern the pattern for this message format
+ * @exception IllegalArgumentException if the pattern is invalid
+ * @see Category#FORMAT
+ * @stable ICU 3.0
+ */
+ public MessageFormat(String pattern) {
+ this.ulocale = ULocale.getDefault(Category.FORMAT);
+ applyPattern(pattern);
+ }
+
+ /**
+ * Constructs a MessageFormat for the specified locale and
+ * pattern.
+ * Sets the locale and calls applyPattern(pattern).
+ *
+ * @param pattern the pattern for this message format
+ * @param locale the locale for this message format
+ * @exception IllegalArgumentException if the pattern is invalid
+ * @stable ICU 3.0
+ */
+ public MessageFormat(String pattern, Locale locale) {
+ this(pattern, ULocale.forLocale(locale));
+ }
+
+ /**
+ * Constructs a MessageFormat for the specified locale and
+ * pattern.
+ * Sets the locale and calls applyPattern(pattern).
+ *
+ * @param pattern the pattern for this message format
+ * @param locale the locale for this message format
+ * @exception IllegalArgumentException if the pattern is invalid
+ * @stable ICU 3.2
+ */
+ public MessageFormat(String pattern, ULocale locale) {
+ this.ulocale = locale;
+ applyPattern(pattern);
+ }
+
+ /**
+ * Sets the locale to be used for creating argument Format objects.
+ * This affects subsequent calls to the {@link #applyPattern applyPattern}
+ * method as well as to the <code>format</code> and
+ * {@link #formatToCharacterIterator formatToCharacterIterator} methods.
+ *
+ * @param locale the locale to be used when creating or comparing subformats
+ * @stable ICU 3.0
+ */
+ public void setLocale(Locale locale) {
+ setLocale(ULocale.forLocale(locale));
+ }
+
+ /**
+ * Sets the locale to be used for creating argument Format objects.
+ * This affects subsequent calls to the {@link #applyPattern applyPattern}
+ * method as well as to the <code>format</code> and
+ * {@link #formatToCharacterIterator formatToCharacterIterator} methods.
+ *
+ * @param locale the locale to be used when creating or comparing subformats
+ * @stable ICU 3.2
+ */
+ public void setLocale(ULocale locale) {
+ /* Save the pattern, and then reapply so that */
+ /* we pick up any changes in locale specific */
+ /* elements */
+ String existingPattern = toPattern(); /*ibm.3550*/
+ this.ulocale = locale;
+ // Invalidate all stock formatters. They are no longer valid since
+ // the locale has changed.
+ stockDateFormatter = null;
+ stockNumberFormatter = null;
+ pluralProvider = null;
+ ordinalProvider = null;
+ applyPattern(existingPattern); /*ibm.3550*/
+ }
+
+ /**
+ * Returns the locale that's used when creating or comparing subformats.
+ *
+ * @return the locale used when creating or comparing subformats
+ * @stable ICU 3.0
+ */
+ public Locale getLocale() {
+ return ulocale.toLocale();
+ }
+
+ /**
+ * {@icu} Returns the locale that's used when creating argument Format objects.
+ *
+ * @return the locale used when creating or comparing subformats
+ * @stable ICU 3.2
+ */
+ public ULocale getULocale() {
+ return ulocale;
+ }
+
+ /**
+ * Sets the pattern used by this message format.
+ * Parses the pattern and caches Format objects for simple argument types.
+ * Patterns and their interpretation are specified in the
+ * <a href="#patterns">class description</a>.
+ *
+ * @param pttrn the pattern for this message format
+ * @throws IllegalArgumentException if the pattern is invalid
+ * @stable ICU 3.0
+ */
+ public void applyPattern(String pttrn) {
+ try {
+ if (msgPattern == null) {
+ msgPattern = new MessagePattern(pttrn);
+ } else {
+ msgPattern.parse(pttrn);
+ }
+ // Cache the formats that are explicitly mentioned in the message pattern.
+ cacheExplicitFormats();
+ } catch(RuntimeException e) {
+ resetPattern();
+ throw e;
+ }
+ }
+
+ /**
+ * {@icu} Sets the ApostropheMode and the pattern used by this message format.
+ * Parses the pattern and caches Format objects for simple argument types.
+ * Patterns and their interpretation are specified in the
+ * <a href="#patterns">class description</a>.
+ * <p>
+ * This method is best used only once on a given object to avoid confusion about the mode,
+ * and after constructing the object with an empty pattern string to minimize overhead.
+ *
+ * @param pattern the pattern for this message format
+ * @param aposMode the new ApostropheMode
+ * @throws IllegalArgumentException if the pattern is invalid
+ * @see MessagePattern.ApostropheMode
+ * @stable ICU 4.8
+ */
+ public void applyPattern(String pattern, MessagePattern.ApostropheMode aposMode) {
+ if (msgPattern == null) {
+ msgPattern = new MessagePattern(aposMode);
+ } else if (aposMode != msgPattern.getApostropheMode()) {
+ msgPattern.clearPatternAndSetApostropheMode(aposMode);
+ }
+ applyPattern(pattern);
+ }
+
+ /**
+ * {@icu}
+ * @return this instance's ApostropheMode.
+ * @stable ICU 4.8
+ */
+ public MessagePattern.ApostropheMode getApostropheMode() {
+ if (msgPattern == null) {
+ msgPattern = new MessagePattern(); // Sets the default mode.
+ }
+ return msgPattern.getApostropheMode();
+ }
+
+ /**
+ * Returns the applied pattern string.
+ * @return the pattern string
+ * @throws IllegalStateException after custom Format objects have been set
+ * via setFormat() or similar APIs
+ * @stable ICU 3.0
+ */
+ public String toPattern() {
+ // Return the original, applied pattern string, or else "".
+ // Note: This does not take into account
+ // - changes from setFormat() and similar methods, or
+ // - normalization of apostrophes and arguments, for example,
+ // whether some date/time/number formatter was created via a pattern
+ // but is equivalent to the "medium" default format.
+ if (customFormatArgStarts != null) {
+ throw new IllegalStateException(
+ "toPattern() is not supported after custom Format objects "+
+ "have been set via setFormat() or similar APIs");
+ }
+ if (msgPattern == null) {
+ return "";
+ }
+ String originalPattern = msgPattern.getPatternString();
+ return originalPattern == null ? "" : originalPattern;
+ }
+
+ /**
+ * Returns the part index of the next ARG_START after partIndex, or -1 if there is none more.
+ * @param partIndex Part index of the previous ARG_START (initially 0).
+ */
+ private int nextTopLevelArgStart(int partIndex) {
+ if (partIndex != 0) {
+ partIndex = msgPattern.getLimitPartIndex(partIndex);
+ }
+ for (;;) {
+ MessagePattern.Part.Type type = msgPattern.getPartType(++partIndex);
+ if (type == MessagePattern.Part.Type.ARG_START) {
+ return partIndex;
+ }
+ if (type == MessagePattern.Part.Type.MSG_LIMIT) {
+ return -1;
+ }
+ }
+ }
+
+ private boolean argNameMatches(int partIndex, String argName, int argNumber) {
+ Part part = msgPattern.getPart(partIndex);
+ return part.getType() == MessagePattern.Part.Type.ARG_NAME ?
+ msgPattern.partSubstringMatches(part, argName) :
+ part.getValue() == argNumber; // ARG_NUMBER
+ }
+
+ private String getArgName(int partIndex) {
+ Part part = msgPattern.getPart(partIndex);
+ if (part.getType() == MessagePattern.Part.Type.ARG_NAME) {
+ return msgPattern.getSubstring(part);
+ } else {
+ return Integer.toString(part.getValue());
+ }
+ }
+
+ /**
+ * Sets the Format objects to use for the values passed into
+ * <code>format</code> methods or returned from <code>parse</code>
+ * methods. The indices of elements in <code>newFormats</code>
+ * correspond to the argument indices used in the previously set
+ * pattern string.
+ * The order of formats in <code>newFormats</code> thus corresponds to
+ * the order of elements in the <code>arguments</code> array passed
+ * to the <code>format</code> methods or the result array returned
+ * by the <code>parse</code> methods.
+ * <p>
+ * If an argument index is used for more than one format element
+ * in the pattern string, then the corresponding new format is used
+ * for all such format elements. If an argument index is not used
+ * for any format element in the pattern string, then the
+ * corresponding new format is ignored. If fewer formats are provided
+ * than needed, then only the formats for argument indices less
+ * than <code>newFormats.length</code> are replaced.
+ *
+ * This method is only supported if the format does not use
+ * named arguments, otherwise an IllegalArgumentException is thrown.
+ *
+ * @param newFormats the new formats to use
+ * @throws NullPointerException if <code>newFormats</code> is null
+ * @throws IllegalArgumentException if this formatter uses named arguments
+ * @stable ICU 3.0
+ */
+ public void setFormatsByArgumentIndex(Format[] newFormats) {
+ if (msgPattern.hasNamedArguments()) {
+ throw new IllegalArgumentException(
+ "This method is not available in MessageFormat objects " +
+ "that use alphanumeric argument names.");
+ }
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ int argNumber = msgPattern.getPart(partIndex + 1).getValue();
+ if (argNumber < newFormats.length) {
+ setCustomArgStartFormat(partIndex, newFormats[argNumber]);
+ }
+ }
+ }
+
+ /**
+ * {@icu} Sets the Format objects to use for the values passed into
+ * <code>format</code> methods or returned from <code>parse</code>
+ * methods. The keys in <code>newFormats</code> are the argument
+ * names in the previously set pattern string, and the values
+ * are the formats.
+ * <p>
+ * Only argument names from the pattern string are considered.
+ * Extra keys in <code>newFormats</code> that do not correspond
+ * to an argument name are ignored. Similarly, if there is no
+ * format in newFormats for an argument name, the formatter
+ * for that argument remains unchanged.
+ * <p>
+ * This may be called on formats that do not use named arguments.
+ * In this case the map will be queried for key Strings that
+ * represent argument indices, e.g. "0", "1", "2" etc.
+ *
+ * @param newFormats a map from String to Format providing new
+ * formats for named arguments.
+ * @stable ICU 3.8
+ */
+ public void setFormatsByArgumentName(Map<String, Format> newFormats) {
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ String key = getArgName(partIndex + 1);
+ if (newFormats.containsKey(key)) {
+ setCustomArgStartFormat(partIndex, newFormats.get(key));
+ }
+ }
+ }
+
+ /**
+ * Sets the Format objects to use for the format elements in the
+ * previously set pattern string.
+ * The order of formats in <code>newFormats</code> corresponds to
+ * the order of format elements in the pattern string.
+ * <p>
+ * If more formats are provided than needed by the pattern string,
+ * the remaining ones are ignored. If fewer formats are provided
+ * than needed, then only the first <code>newFormats.length</code>
+ * formats are replaced.
+ * <p>
+ * Since the order of format elements in a pattern string often
+ * changes during localization, it is generally better to use the
+ * {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex}
+ * method, which assumes an order of formats corresponding to the
+ * order of elements in the <code>arguments</code> array passed to
+ * the <code>format</code> methods or the result array returned by
+ * the <code>parse</code> methods.
+ *
+ * @param newFormats the new formats to use
+ * @exception NullPointerException if <code>newFormats</code> is null
+ * @stable ICU 3.0
+ */
+ public void setFormats(Format[] newFormats) {
+ int formatNumber = 0;
+ for (int partIndex = 0;
+ formatNumber < newFormats.length &&
+ (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ setCustomArgStartFormat(partIndex, newFormats[formatNumber]);
+ ++formatNumber;
+ }
+ }
+
+ /**
+ * Sets the Format object to use for the format elements within the
+ * previously set pattern string that use the given argument
+ * index.
+ * The argument index is part of the format element definition and
+ * represents an index into the <code>arguments</code> array passed
+ * to the <code>format</code> methods or the result array returned
+ * by the <code>parse</code> methods.
+ * <p>
+ * If the argument index is used for more than one format element
+ * in the pattern string, then the new format is used for all such
+ * format elements. If the argument index is not used for any format
+ * element in the pattern string, then the new format is ignored.
+ *
+ * This method is only supported when exclusively numbers are used for
+ * argument names. Otherwise an IllegalArgumentException is thrown.
+ *
+ * @param argumentIndex the argument index for which to use the new format
+ * @param newFormat the new format to use
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
+ if (msgPattern.hasNamedArguments()) {
+ throw new IllegalArgumentException(
+ "This method is not available in MessageFormat objects " +
+ "that use alphanumeric argument names.");
+ }
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ if (msgPattern.getPart(partIndex + 1).getValue() == argumentIndex) {
+ setCustomArgStartFormat(partIndex, newFormat);
+ }
+ }
+ }
+
+ /**
+ * {@icu} Sets the Format object to use for the format elements within the
+ * previously set pattern string that use the given argument
+ * name.
+ * <p>
+ * If the argument name is used for more than one format element
+ * in the pattern string, then the new format is used for all such
+ * format elements. If the argument name is not used for any format
+ * element in the pattern string, then the new format is ignored.
+ * <p>
+ * This API may be used on formats that do not use named arguments.
+ * In this case <code>argumentName</code> should be a String that names
+ * an argument index, e.g. "0", "1", "2"... etc. If it does not name
+ * a valid index, the format will be ignored. No error is thrown.
+ *
+ * @param argumentName the name of the argument to change
+ * @param newFormat the new format to use
+ * @stable ICU 3.8
+ */
+ public void setFormatByArgumentName(String argumentName, Format newFormat) {
+ int argNumber = MessagePattern.validateArgumentName(argumentName);
+ if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
+ return;
+ }
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
+ setCustomArgStartFormat(partIndex, newFormat);
+ }
+ }
+ }
+
+ /**
+ * Sets the Format object to use for the format element with the given
+ * format element index within the previously set pattern string.
+ * The format element index is the zero-based number of the format
+ * element counting from the start of the pattern string.
+ * <p>
+ * Since the order of format elements in a pattern string often
+ * changes during localization, it is generally better to use the
+ * {@link #setFormatByArgumentIndex setFormatByArgumentIndex}
+ * method, which accesses format elements based on the argument
+ * index they specify.
+ *
+ * @param formatElementIndex the index of a format element within the pattern
+ * @param newFormat the format to use for the specified format element
+ * @exception ArrayIndexOutOfBoundsException if formatElementIndex is equal to or
+ * larger than the number of format elements in the pattern string
+ * @stable ICU 3.0
+ */
+ public void setFormat(int formatElementIndex, Format newFormat) {
+ int formatNumber = 0;
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ if (formatNumber == formatElementIndex) {
+ setCustomArgStartFormat(partIndex, newFormat);
+ return;
+ }
+ ++formatNumber;
+ }
+ throw new ArrayIndexOutOfBoundsException(formatElementIndex);
+ }
+
+ /**
+ * Returns the Format objects used for the values passed into
+ * <code>format</code> methods or returned from <code>parse</code>
+ * methods. The indices of elements in the returned array
+ * correspond to the argument indices used in the previously set
+ * pattern string.
+ * The order of formats in the returned array thus corresponds to
+ * the order of elements in the <code>arguments</code> array passed
+ * to the <code>format</code> methods or the result array returned
+ * by the <code>parse</code> methods.
+ * <p>
+ * If an argument index is used for more than one format element
+ * in the pattern string, then the format used for the last such
+ * format element is returned in the array. If an argument index
+ * is not used for any format element in the pattern string, then
+ * null is returned in the array.
+ *
+ * This method is only supported when exclusively numbers are used for
+ * argument names. Otherwise an IllegalArgumentException is thrown.
+ *
+ * @return the formats used for the arguments within the pattern
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public Format[] getFormatsByArgumentIndex() {
+ if (msgPattern.hasNamedArguments()) {
+ throw new IllegalArgumentException(
+ "This method is not available in MessageFormat objects " +
+ "that use alphanumeric argument names.");
+ }
+ ArrayList<Format> list = new ArrayList<Format>();
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ int argNumber = msgPattern.getPart(partIndex + 1).getValue();
+ while (argNumber >= list.size()) {
+ list.add(null);
+ }
+ list.set(argNumber, cachedFormatters == null ? null : cachedFormatters.get(partIndex));
+ }
+ return list.toArray(new Format[list.size()]);
+ }
+
+ /**
+ * Returns the Format objects used for the format elements in the
+ * previously set pattern string.
+ * The order of formats in the returned array corresponds to
+ * the order of format elements in the pattern string.
+ * <p>
+ * Since the order of format elements in a pattern string often
+ * changes during localization, it's generally better to use the
+ * {@link #getFormatsByArgumentIndex()}
+ * method, which assumes an order of formats corresponding to the
+ * order of elements in the <code>arguments</code> array passed to
+ * the <code>format</code> methods or the result array returned by
+ * the <code>parse</code> methods.
+ *
+ * This method is only supported when exclusively numbers are used for
+ * argument names. Otherwise an IllegalArgumentException is thrown.
+ *
+ * @return the formats used for the format elements in the pattern
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public Format[] getFormats() {
+ ArrayList<Format> list = new ArrayList<Format>();
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ list.add(cachedFormatters == null ? null : cachedFormatters.get(partIndex));
+ }
+ return list.toArray(new Format[list.size()]);
+ }
+
+ /**
+ * {@icu} Returns the top-level argument names. For more details, see
+ * {@link #setFormatByArgumentName(String, Format)}.
+ * @return a Set of argument names
+ * @stable ICU 4.8
+ */
+ public Set<String> getArgumentNames() {
+ Set<String> result = new HashSet<String>();
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ result.add(getArgName(partIndex + 1));
+ }
+ return result;
+ }
+
+ /**
+ * {@icu} Returns the first top-level format associated with the given argument name.
+ * For more details, see {@link #setFormatByArgumentName(String, Format)}.
+ * @param argumentName The name of the desired argument.
+ * @return the Format associated with the name, or null if there isn't one.
+ * @stable ICU 4.8
+ */
+ public Format getFormatByArgumentName(String argumentName) {
+ if (cachedFormatters == null) {
+ return null;
+ }
+ int argNumber = MessagePattern.validateArgumentName(argumentName);
+ if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
+ return null;
+ }
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
+ return cachedFormatters.get(partIndex);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Formats an array of objects and appends the <code>MessageFormat</code>'s
+ * pattern, with arguments replaced by the formatted objects, to the
+ * provided <code>StringBuffer</code>.
+ * <p>
+ * The text substituted for the individual format elements is derived from
+ * the current subformat of the format element and the
+ * <code>arguments</code> element at the format element's argument index
+ * as indicated by the first matching line of the following table. An
+ * argument is <i>unavailable</i> if <code>arguments</code> is
+ * <code>null</code> or has fewer than argumentIndex+1 elements. When
+ * an argument is unavailable no substitution is performed.
+ * <p>
+ * <table border=1>
+ * <tr>
+ * <th>argType or Format
+ * <th>value object
+ * <th>Formatted Text
+ * <tr>
+ * <td><i>any</i>
+ * <td><i>unavailable</i>
+ * <td><code>"{" + argNameOrNumber + "}"</code>
+ * <tr>
+ * <td><i>any</i>
+ * <td><code>null</code>
+ * <td><code>"null"</code>
+ * <tr>
+ * <td>custom Format <code>!= null</code>
+ * <td><i>any</i>
+ * <td><code>customFormat.format(argument)</code>
+ * <tr>
+ * <td>noneArg, or custom Format <code>== null</code>
+ * <td><code>instanceof Number</code>
+ * <td><code>NumberFormat.getInstance(getLocale()).format(argument)</code>
+ * <tr>
+ * <td>noneArg, or custom Format <code>== null</code>
+ * <td><code>instanceof Date</code>
+ * <td><code>DateFormat.getDateTimeInstance(DateFormat.SHORT,
+ * DateFormat.SHORT, getLocale()).format(argument)</code>
+ * <tr>
+ * <td>noneArg, or custom Format <code>== null</code>
+ * <td><code>instanceof String</code>
+ * <td><code>argument</code>
+ * <tr>
+ * <td>noneArg, or custom Format <code>== null</code>
+ * <td><i>any</i>
+ * <td><code>argument.toString()</code>
+ * <tr>
+ * <td>complexArg
+ * <td><i>any</i>
+ * <td>result of recursive formatting of a selected sub-message
+ * </table>
+ * <p>
+ * If <code>pos</code> is non-null, and refers to
+ * <code>Field.ARGUMENT</code>, the location of the first formatted
+ * string will be returned.
+ *
+ * This method is only supported when the format does not use named
+ * arguments, otherwise an IllegalArgumentException is thrown.
+ *
+ * @param arguments an array of objects to be formatted and substituted.
+ * @param result where text is appended.
+ * @param pos On input: an alignment field, if desired.
+ * On output: the offsets of the alignment field.
+ * @throws IllegalArgumentException if a value in the
+ * <code>arguments</code> array is not of the type
+ * expected by the corresponding argument or custom Format object.
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public final StringBuffer format(Object[] arguments, StringBuffer result,
+ FieldPosition pos)
+ {
+ format(arguments, null, new AppendableWrapper(result), pos);
+ return result;
+ }
+
+ /**
+ * Formats a map of objects and appends the <code>MessageFormat</code>'s
+ * pattern, with arguments replaced by the formatted objects, to the
+ * provided <code>StringBuffer</code>.
+ * <p>
+ * The text substituted for the individual format elements is derived from
+ * the current subformat of the format element and the
+ * <code>arguments</code> value corresopnding to the format element's
+ * argument name.
+ * <p>
+ * A numbered pattern argument is matched with a map key that contains that number
+ * as an ASCII-decimal-digit string (without leading zero).
+ * <p>
+ * An argument is <i>unavailable</i> if <code>arguments</code> is
+ * <code>null</code> or does not have a value corresponding to an argument
+ * name in the pattern. When an argument is unavailable no substitution
+ * is performed.
+ *
+ * @param arguments a map of objects to be formatted and substituted.
+ * @param result where text is appended.
+ * @param pos On input: an alignment field, if desired.
+ * On output: the offsets of the alignment field.
+ * @throws IllegalArgumentException if a value in the
+ * <code>arguments</code> array is not of the type
+ * expected by the corresponding argument or custom Format object.
+ * @return the passed-in StringBuffer
+ * @stable ICU 3.8
+ */
+ public final StringBuffer format(Map<String, Object> arguments, StringBuffer result,
+ FieldPosition pos) {
+ format(null, arguments, new AppendableWrapper(result), pos);
+ return result;
+ }
+
+ /**
+ * Creates a MessageFormat with the given pattern and uses it
+ * to format the given arguments. This is equivalent to
+ * <blockquote>
+ * <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link
+ * #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition)
+ * format}(arguments, new StringBuffer(), null).toString()</code>
+ * </blockquote>
+ *
+ * @throws IllegalArgumentException if the pattern is invalid
+ * @throws IllegalArgumentException if a value in the
+ * <code>arguments</code> array is not of the type
+ * expected by the corresponding argument or custom Format object.
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public static String format(String pattern, Object... arguments) {
+ MessageFormat temp = new MessageFormat(pattern);
+ return temp.format(arguments);
+ }
+
+ /**
+ * Creates a MessageFormat with the given pattern and uses it to
+ * format the given arguments. The pattern must identifyarguments
+ * by name instead of by number.
+ * <p>
+ * @throws IllegalArgumentException if the pattern is invalid
+ * @throws IllegalArgumentException if a value in the
+ * <code>arguments</code> array is not of the type
+ * expected by the corresponding argument or custom Format object.
+ * @see #format(Map, StringBuffer, FieldPosition)
+ * @see #format(String, Object[])
+ * @stable ICU 3.8
+ */
+ public static String format(String pattern, Map<String, Object> arguments) {
+ MessageFormat temp = new MessageFormat(pattern);
+ return temp.format(arguments);
+ }
+
+ /**
+ * {@icu} Returns true if this MessageFormat uses named arguments,
+ * and false otherwise. See class description.
+ *
+ * @return true if named arguments are used.
+ * @stable ICU 3.8
+ */
+ public boolean usesNamedArguments() {
+ return msgPattern.hasNamedArguments();
+ }
+
+ // Overrides
+ /**
+ * Formats a map or array of objects and appends the <code>MessageFormat</code>'s
+ * pattern, with format elements replaced by the formatted objects, to the
+ * provided <code>StringBuffer</code>.
+ * This is equivalent to either of
+ * <blockquote>
+ * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
+ * java.text.FieldPosition) format}((Object[]) arguments, result, pos)</code>
+ * <code>{@link #format(java.util.Map, java.lang.StringBuffer,
+ * java.text.FieldPosition) format}((Map) arguments, result, pos)</code>
+ * </blockquote>
+ * A map must be provided if this format uses named arguments, otherwise
+ * an IllegalArgumentException will be thrown.
+ * @param arguments a map or array of objects to be formatted
+ * @param result where text is appended
+ * @param pos On input: an alignment field, if desired
+ * On output: the offsets of the alignment field
+ * @throws IllegalArgumentException if an argument in
+ * <code>arguments</code> is not of the type
+ * expected by the format element(s) that use it
+ * @throws IllegalArgumentException if <code>arguments<code> is
+ * an array of Object and this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public final StringBuffer format(Object arguments, StringBuffer result,
+ FieldPosition pos)
+ {
+ format(arguments, new AppendableWrapper(result), pos);
+ return result;
+ }
+
+ /**
+ * Formats an array of objects and inserts them into the
+ * <code>MessageFormat</code>'s pattern, producing an
+ * <code>AttributedCharacterIterator</code>.
+ * You can use the returned <code>AttributedCharacterIterator</code>
+ * to build the resulting String, as well as to determine information
+ * about the resulting String.
+ * <p>
+ * The text of the returned <code>AttributedCharacterIterator</code> is
+ * the same that would be returned by
+ * <blockquote>
+ * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
+ * java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()</code>
+ * </blockquote>
+ * <p>
+ * In addition, the <code>AttributedCharacterIterator</code> contains at
+ * least attributes indicating where text was generated from an
+ * argument in the <code>arguments</code> array. The keys of these attributes are of
+ * type <code>MessageFormat.Field</code>, their values are
+ * <code>Integer</code> objects indicating the index in the <code>arguments</code>
+ * array of the argument from which the text was generated.
+ * <p>
+ * The attributes/value from the underlying <code>Format</code>
+ * instances that <code>MessageFormat</code> uses will also be
+ * placed in the resulting <code>AttributedCharacterIterator</code>.
+ * This allows you to not only find where an argument is placed in the
+ * resulting String, but also which fields it contains in turn.
+ *
+ * @param arguments an array of objects to be formatted and substituted.
+ * @return AttributedCharacterIterator describing the formatted value.
+ * @exception NullPointerException if <code>arguments</code> is null.
+ * @throws IllegalArgumentException if a value in the
+ * <code>arguments</code> array is not of the type
+ * expected by the corresponding argument or custom Format object.
+ * @stable ICU 3.8
+ */
+ public AttributedCharacterIterator formatToCharacterIterator(Object arguments) {
+ if (arguments == null) {
+ throw new NullPointerException(
+ "formatToCharacterIterator must be passed non-null object");
+ }
+ StringBuilder result = new StringBuilder();
+ AppendableWrapper wrapper = new AppendableWrapper(result);
+ wrapper.useAttributes();
+ format(arguments, wrapper, null);
+ AttributedString as = new AttributedString(result.toString());
+ for (AttributeAndPosition a : wrapper.attributes) {
+ as.addAttribute(a.key, a.value, a.start, a.limit);
+ }
+ return as.getIterator();
+ }
+
+ /**
+ * Parses the string.
+ *
+ * <p>Caveats: The parse may fail in a number of circumstances.
+ * For example:
+ * <ul>
+ * <li>If one of the arguments does not occur in the pattern.
+ * <li>If the format of an argument loses information, such as
+ * with a choice format where a large number formats to "many".
+ * <li>Does not yet handle recursion (where
+ * the substituted strings contain {n} references.)
+ * <li>Will not always find a match (or the correct match)
+ * if some part of the parse is ambiguous.
+ * For example, if the pattern "{1},{2}" is used with the
+ * string arguments {"a,b", "c"}, it will format as "a,b,c".
+ * When the result is parsed, it will return {"a", "b,c"}.
+ * <li>If a single argument is parsed more than once in the string,
+ * then the later parse wins.
+ * </ul>
+ * When the parse fails, use ParsePosition.getErrorIndex() to find out
+ * where in the string did the parsing failed. The returned error
+ * index is the starting offset of the sub-patterns that the string
+ * is comparing with. For example, if the parsing string "AAA {0} BBB"
+ * is comparing against the pattern "AAD {0} BBB", the error index is
+ * 0. When an error occurs, the call to this method will return null.
+ * If the source is null, return an empty array.
+ *
+ * @throws IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public Object[] parse(String source, ParsePosition pos) {
+ if (msgPattern.hasNamedArguments()) {
+ throw new IllegalArgumentException(
+ "This method is not available in MessageFormat objects " +
+ "that use named argument.");
+ }
+
+ // Count how many slots we need in the array.
+ int maxArgId = -1;
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ int argNumber=msgPattern.getPart(partIndex + 1).getValue();
+ if (argNumber > maxArgId) {
+ maxArgId = argNumber;
+ }
+ }
+ Object[] resultArray = new Object[maxArgId + 1];
+
+ int backupStartPos = pos.getIndex();
+ parse(0, source, pos, resultArray, null);
+ if (pos.getIndex() == backupStartPos) { // unchanged, returned object is null
+ return null;
+ }
+
+ return resultArray;
+ }
+
+ /**
+ * {@icu} Parses the string, returning the results in a Map.
+ * This is similar to the version that returns an array
+ * of Object. This supports both named and numbered
+ * arguments-- if numbered, the keys in the map are the
+ * corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...).
+ *
+ * @param source the text to parse
+ * @param pos the position at which to start parsing. on return,
+ * contains the result of the parse.
+ * @return a Map containing key/value pairs for each parsed argument.
+ * @stable ICU 3.8
+ */
+ public Map<String, Object> parseToMap(String source, ParsePosition pos) {
+ Map<String, Object> result = new HashMap<String, Object>();
+ int backupStartPos = pos.getIndex();
+ parse(0, source, pos, null, result);
+ if (pos.getIndex() == backupStartPos) {
+ return null;
+ }
+ return result;
+ }
+
+ /**
+ * Parses text from the beginning of the given string to produce an object
+ * array.
+ * The method may not use the entire text of the given string.
+ * <p>
+ * See the {@link #parse(String, ParsePosition)} method for more information
+ * on message parsing.
+ *
+ * @param source A <code>String</code> whose beginning should be parsed.
+ * @return An <code>Object</code> array parsed from the string.
+ * @exception ParseException if the beginning of the specified string cannot be parsed.
+ * @exception IllegalArgumentException if this format uses named arguments
+ * @stable ICU 3.0
+ */
+ public Object[] parse(String source) throws ParseException {
+ ParsePosition pos = new ParsePosition(0);
+ Object[] result = parse(source, pos);
+ if (pos.getIndex() == 0) // unchanged, returned object is null
+ throw new ParseException("MessageFormat parse error!",
+ pos.getErrorIndex());
+
+ return result;
+ }
+
+ /**
+ * Parses the string, filling either the Map or the Array.
+ * This is a private method that all the public parsing methods call.
+ * This supports both named and numbered
+ * arguments-- if numbered, the keys in the map are the
+ * corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...).
+ *
+ * @param msgStart index in the message pattern to start from.
+ * @param source the text to parse
+ * @param pos the position at which to start parsing. on return,
+ * contains the result of the parse.
+ * @param args if not null, the parse results will be filled here (The pattern
+ * has to have numbered arguments in order for this to not be null).
+ * @param argsMap if not null, the parse results will be filled here.
+ */
+ private void parse(int msgStart, String source, ParsePosition pos,
+ Object[] args, Map<String, Object> argsMap) {
+ if (source == null) {
+ return;
+ }
+ String msgString=msgPattern.getPatternString();
+ int prevIndex=msgPattern.getPart(msgStart).getLimit();
+ int sourceOffset = pos.getIndex();
+ ParsePosition tempStatus = new ParsePosition(0);
+
+ for(int i=msgStart+1; ; ++i) {
+ Part part=msgPattern.getPart(i);
+ Part.Type type=part.getType();
+ int index=part.getIndex();
+ // Make sure the literal string matches.
+ int len = index - prevIndex;
+ if (len == 0 || msgString.regionMatches(prevIndex, source, sourceOffset, len)) {
+ sourceOffset += len;
+ prevIndex += len;
+ } else {
+ pos.setErrorIndex(sourceOffset);
+ return; // leave index as is to signal error
+ }
+ if(type==Part.Type.MSG_LIMIT) {
+ // Things went well! Done.
+ pos.setIndex(sourceOffset);
+ return;
+ }
+ if(type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR) {
+ prevIndex=part.getLimit();
+ continue;
+ }
+ // We do not support parsing Plural formats. (No REPLACE_NUMBER here.)
+ assert type==Part.Type.ARG_START : "Unexpected Part "+part+" in parsed message.";
+ int argLimit=msgPattern.getLimitPartIndex(i);
+
+ ArgType argType=part.getArgType();
+ part=msgPattern.getPart(++i);
+ // Compute the argId, so we can use it as a key.
+ Object argId=null;
+ int argNumber = 0;
+ String key = null;
+ if(args!=null) {
+ argNumber=part.getValue(); // ARG_NUMBER
+ argId = Integer.valueOf(argNumber);
+ } else {
+ if(part.getType()==MessagePattern.Part.Type.ARG_NAME) {
+ key=msgPattern.getSubstring(part);
+ } else /* ARG_NUMBER */ {
+ key=Integer.toString(part.getValue());
+ }
+ argId = key;
+ }
+
+ ++i;
+ Format formatter = null;
+ boolean haveArgResult = false;
+ Object argResult = null;
+ if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) {
+ // Just parse using the formatter.
+ tempStatus.setIndex(sourceOffset);
+ argResult = formatter.parseObject(source, tempStatus);
+ if (tempStatus.getIndex() == sourceOffset) {
+ pos.setErrorIndex(sourceOffset);
+ return; // leave index as is to signal error
+ }
+ haveArgResult = true;
+ sourceOffset = tempStatus.getIndex();
+ } else if(
+ argType==ArgType.NONE ||
+ (cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) {
+ // Match as a string.
+ // if at end, use longest possible match
+ // otherwise uses first match to intervening string
+ // does NOT recursively try all possibilities
+ String stringAfterArgument = getLiteralStringUntilNextArgument(argLimit);
+ int next;
+ if (stringAfterArgument.length() != 0) {
+ next = source.indexOf(stringAfterArgument, sourceOffset);
+ } else {
+ next = source.length();
+ }
+ if (next < 0) {
+ pos.setErrorIndex(sourceOffset);
+ return; // leave index as is to signal error
+ } else {
+ String strValue = source.substring(sourceOffset, next);
+ if (!strValue.equals("{" + argId.toString() + "}")) {
+ haveArgResult = true;
+ argResult = strValue;
+ }
+ sourceOffset = next;
+ }
+ } else if(argType==ArgType.CHOICE) {
+ tempStatus.setIndex(sourceOffset);
+ double choiceResult = parseChoiceArgument(msgPattern, i, source, tempStatus);
+ if (tempStatus.getIndex() == sourceOffset) {
+ pos.setErrorIndex(sourceOffset);
+ return; // leave index as is to signal error
+ }
+ argResult = choiceResult;
+ haveArgResult = true;
+ sourceOffset = tempStatus.getIndex();
+ } else if(argType.hasPluralStyle() || argType==ArgType.SELECT) {
+ // No can do!
+ throw new UnsupportedOperationException(
+ "Parsing of plural/select/selectordinal argument is not supported.");
+ } else {
+ // This should never happen.
+ throw new IllegalStateException("unexpected argType "+argType);
+ }
+ if (haveArgResult) {
+ if (args != null) {
+ args[argNumber] = argResult;
+ } else if (argsMap != null) {
+ argsMap.put(key, argResult);
+ }
+ }
+ prevIndex=msgPattern.getPart(argLimit).getLimit();
+ i=argLimit;
+ }
+ }
+
+ /**
+ * {@icu} Parses text from the beginning of the given string to produce a map from
+ * argument to values. The method may not use the entire text of the given string.
+ *
+ * <p>See the {@link #parse(String, ParsePosition)} method for more information on
+ * message parsing.
+ *
+ * @param source A <code>String</code> whose beginning should be parsed.
+ * @return A <code>Map</code> parsed from the string.
+ * @throws ParseException if the beginning of the specified string cannot
+ * be parsed.
+ * @see #parseToMap(String, ParsePosition)
+ * @stable ICU 3.8
+ */
+ public Map<String, Object> parseToMap(String source) throws ParseException {
+ ParsePosition pos = new ParsePosition(0);
+ Map<String, Object> result = new HashMap<String, Object>();
+ parse(0, source, pos, null, result);
+ if (pos.getIndex() == 0) // unchanged, returned object is null
+ throw new ParseException("MessageFormat parse error!",
+ pos.getErrorIndex());
+
+ return result;
+ }
+
+ /**
+ * Parses text from a string to produce an object array or Map.
+ * <p>
+ * The method attempts to parse text starting at the index given by
+ * <code>pos</code>.
+ * If parsing succeeds, then the index of <code>pos</code> is updated
+ * to the index after the last character used (parsing does not necessarily
+ * use all characters up to the end of the string), and the parsed
+ * object array is returned. The updated <code>pos</code> can be used to
+ * indicate the starting point for the next call to this method.
+ * If an error occurs, then the index of <code>pos</code> is not
+ * changed, the error index of <code>pos</code> is set to the index of
+ * the character where the error occurred, and null is returned.
+ * <p>
+ * See the {@link #parse(String, ParsePosition)} method for more information
+ * on message parsing.
+ *
+ * @param source A <code>String</code>, part of which should be parsed.
+ * @param pos A <code>ParsePosition</code> object with index and error
+ * index information as described above.
+ * @return An <code>Object</code> parsed from the string, either an
+ * array of Object, or a Map, depending on whether named
+ * arguments are used. This can be queried using <code>usesNamedArguments</code>.
+ * In case of error, returns null.
+ * @throws NullPointerException if <code>pos</code> is null.
+ * @stable ICU 3.0
+ */
+ public Object parseObject(String source, ParsePosition pos) {
+ if (!msgPattern.hasNamedArguments()) {
+ return parse(source, pos);
+ } else {
+ return parseToMap(source, pos);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 3.0
+ */
+ @Override
+ public Object clone() {
+ MessageFormat other = (MessageFormat) super.clone();
+
+ if (customFormatArgStarts != null) {
+ other.customFormatArgStarts = new HashSet<Integer>();
+ for (Integer key : customFormatArgStarts) {
+ other.customFormatArgStarts.add(key);
+ }
+ } else {
+ other.customFormatArgStarts = null;
+ }
+
+ if (cachedFormatters != null) {
+ other.cachedFormatters = new HashMap<Integer, Format>();
+ Iterator<Map.Entry<Integer, Format>> it = cachedFormatters.entrySet().iterator();
+ while (it.hasNext()){
+ Map.Entry<Integer, Format> entry = it.next();
+ other.cachedFormatters.put(entry.getKey(), entry.getValue());
+ }
+ } else {
+ other.cachedFormatters = null;
+ }
+
+ other.msgPattern = msgPattern == null ? null : (MessagePattern)msgPattern.clone();
+ other.stockDateFormatter =
+ stockDateFormatter == null ? null : (DateFormat) stockDateFormatter.clone();
+ other.stockNumberFormatter =
+ stockNumberFormatter == null ? null : (NumberFormat) stockNumberFormatter.clone();
+
+ other.pluralProvider = null;
+ other.ordinalProvider = null;
+ return other;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 3.0
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) // quick check
+ return true;
+ if (obj == null || getClass() != obj.getClass())
+ return false;
+ MessageFormat other = (MessageFormat) obj;
+ return Utility.objectEquals(ulocale, other.ulocale)
+ && Utility.objectEquals(msgPattern, other.msgPattern)
+ && Utility.objectEquals(cachedFormatters, other.cachedFormatters)
+ && Utility.objectEquals(customFormatArgStarts, other.customFormatArgStarts);
+ // Note: It might suffice to only compare custom formatters
+ // rather than all formatters.
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 3.0
+ */
+ @Override
+ public int hashCode() {
+ return msgPattern.getPatternString().hashCode(); // enough for reasonable distribution
+ }
+
+ /**
+ * Defines constants that are used as attribute keys in the
+ * <code>AttributedCharacterIterator</code> returned
+ * from <code>MessageFormat.formatToCharacterIterator</code>.
+ *
+ * @stable ICU 3.8
+ */
+ public static class Field extends Format.Field {
+
+ private static final long serialVersionUID = 7510380454602616157L;
+
+ /**
+ * Create a <code>Field</code> with the specified name.
+ *
+ * @param name The name of the attribute
+ *
+ * @stable ICU 3.8
+ */
+ protected Field(String name) {
+ super(name);
+ }
+
+ /**
+ * Resolves instances being deserialized to the predefined constants.
+ *
+ * @return resolved MessageFormat.Field constant
+ * @throws InvalidObjectException if the constant could not be resolved.
+ *
+ * @stable ICU 3.8
+ */
+ protected Object readResolve() throws InvalidObjectException {
+ if (this.getClass() != MessageFormat.Field.class) {
+ throw new InvalidObjectException(
+ "A subclass of MessageFormat.Field must implement readResolve.");
+ }
+ if (this.getName().equals(ARGUMENT.getName())) {
+ return ARGUMENT;
+ } else {
+ throw new InvalidObjectException("Unknown attribute name.");
+ }
+ }
+
+ /**
+ * Constant identifying a portion of a message that was generated
+ * from an argument passed into <code>formatToCharacterIterator</code>.
+ * The value associated with the key will be an <code>Integer</code>
+ * indicating the index in the <code>arguments</code> array of the
+ * argument from which the text was generated.
+ *
+ * @stable ICU 3.8
+ */
+ public static final Field ARGUMENT = new Field("message argument field");
+ }
+
+ // ===========================privates============================
+
+ // *Important*: All fields must be declared *transient* so that we can fully
+ // control serialization!
+ // See for example Joshua Bloch's "Effective Java", chapter 10 Serialization.
+
+ /**
+ * The locale to use for formatting numbers and dates.
+ */
+ private transient ULocale ulocale;
+
+ /**
+ * The MessagePattern which contains the parsed structure of the pattern string.
+ */
+ private transient MessagePattern msgPattern;
+ /**
+ * Cached formatters so we can just use them whenever needed instead of creating
+ * them from scratch every time.
+ */
+ private transient Map<Integer, Format> cachedFormatters;
+ /**
+ * Set of ARG_START part indexes where custom, user-provided Format objects
+ * have been set via setFormat() or similar API.
+ */
+ private transient Set<Integer> customFormatArgStarts;
+
+ /**
+ * Stock formatters. Those are used when a format is not explicitly mentioned in
+ * the message. The format is inferred from the argument.
+ */
+ private transient DateFormat stockDateFormatter;
+ private transient NumberFormat stockNumberFormatter;
+
+ private transient PluralSelectorProvider pluralProvider;
+ private transient PluralSelectorProvider ordinalProvider;
+
+ private DateFormat getStockDateFormatter() {
+ if (stockDateFormatter == null) {
+ stockDateFormatter = DateFormat.getDateTimeInstance(
+ DateFormat.SHORT, DateFormat.SHORT, ulocale);//fix
+ }
+ return stockDateFormatter;
+ }
+ private NumberFormat getStockNumberFormatter() {
+ if (stockNumberFormatter == null) {
+ stockNumberFormatter = NumberFormat.getInstance(ulocale);
+ }
+ return stockNumberFormatter;
+ }
+
+ // *Important*: All fields must be declared *transient*.
+ // See the longer comment above ulocale.
+
+ /**
+ * Formats the arguments and writes the result into the
+ * AppendableWrapper, updates the field position.
+ *
+ * <p>Exactly one of args and argsMap must be null, the other non-null.
+ *
+ * @param msgStart Index to msgPattern part to start formatting from.
+ * @param pluralNumber null except when formatting a plural argument sub-message
+ * where a '#' is replaced by the format string for this number.
+ * @param args The formattable objects array. Non-null iff numbered values are used.
+ * @param argsMap The key-value map of formattable objects. Non-null iff named values are used.
+ * @param dest Output parameter to receive the result.
+ * The result (string & attributes) is appended to existing contents.
+ * @param fp Field position status.
+ */
+ private void format(int msgStart, PluralSelectorContext pluralNumber,
+ Object[] args, Map<String, Object> argsMap,
+ AppendableWrapper dest, FieldPosition fp) {
+ String msgString=msgPattern.getPatternString();
+ int prevIndex=msgPattern.getPart(msgStart).getLimit();
+ for(int i=msgStart+1;; ++i) {
+ Part part=msgPattern.getPart(i);
+ Part.Type type=part.getType();
+ int index=part.getIndex();
+ dest.append(msgString, prevIndex, index);
+ if(type==Part.Type.MSG_LIMIT) {
+ return;
+ }
+ prevIndex=part.getLimit();
+ if(type==Part.Type.REPLACE_NUMBER) {
+ if(pluralNumber.forReplaceNumber) {
+ // number-offset was already formatted.
+ dest.formatAndAppend(pluralNumber.formatter,
+ pluralNumber.number, pluralNumber.numberString);
+ } else {
+ dest.formatAndAppend(getStockNumberFormatter(), pluralNumber.number);
+ }
+ continue;
+ }
+ if(type!=Part.Type.ARG_START) {
+ continue;
+ }
+ int argLimit=msgPattern.getLimitPartIndex(i);
+ ArgType argType=part.getArgType();
+ part=msgPattern.getPart(++i);
+ Object arg;
+ boolean noArg=false;
+ Object argId=null;
+ String argName=msgPattern.getSubstring(part);
+ if(args!=null) {
+ int argNumber=part.getValue(); // ARG_NUMBER
+ if (dest.attributes != null) {
+ // We only need argId if we add it into the attributes.
+ argId = Integer.valueOf(argNumber);
+ }
+ if(0<=argNumber && argNumber<args.length) {
+ arg=args[argNumber];
+ } else {
+ arg=null;
+ noArg=true;
+ }
+ } else {
+ argId = argName;
+ if(argsMap!=null && argsMap.containsKey(argName)) {
+ arg=argsMap.get(argName);
+ } else {
+ arg=null;
+ noArg=true;
+ }
+ }
+ ++i;
+ int prevDestLength=dest.length;
+ Format formatter = null;
+ if (noArg) {
+ dest.append("{"+argName+"}");
+ } else if (arg == null) {
+ dest.append("null");
+ } else if(pluralNumber!=null && pluralNumber.numberArgIndex==(i-2)) {
+ if(pluralNumber.offset == 0) {
+ // The number was already formatted with this formatter.
+ dest.formatAndAppend(pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString);
+ } else {
+ // Do not use the formatted (number-offset) string for a named argument
+ // that formats the number without subtracting the offset.
+ dest.formatAndAppend(pluralNumber.formatter, arg);
+ }
+ } else if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) {
+ // Handles all ArgType.SIMPLE, and formatters from setFormat() and its siblings.
+ if ( formatter instanceof ChoiceFormat ||
+ formatter instanceof PluralFormat ||
+ formatter instanceof SelectFormat) {
+ // We only handle nested formats here if they were provided via setFormat() or its siblings.
+ // Otherwise they are not cached and instead handled below according to argType.
+ String subMsgString = formatter.format(arg);
+ if (subMsgString.indexOf('{') >= 0 ||
+ (subMsgString.indexOf('\'') >= 0 && !msgPattern.jdkAposMode())) {
+ MessageFormat subMsgFormat = new MessageFormat(subMsgString, ulocale);
+ subMsgFormat.format(0, null, args, argsMap, dest, null);
+ } else if (dest.attributes == null) {
+ dest.append(subMsgString);
+ } else {
+ // This formats the argument twice, once above to get the subMsgString
+ // and then once more here.
+ // It only happens in formatToCharacterIterator()
+ // on a complex Format set via setFormat(),
+ // and only when the selected subMsgString does not need further formatting.
+ // This imitates ICU 4.6 behavior.
+ dest.formatAndAppend(formatter, arg);
+ }
+ } else {
+ dest.formatAndAppend(formatter, arg);
+ }
+ } else if(
+ argType==ArgType.NONE ||
+ (cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) {
+ // ArgType.NONE, or
+ // any argument which got reset to null via setFormat() or its siblings.
+ if (arg instanceof Number) {
+ // format number if can
+ dest.formatAndAppend(getStockNumberFormatter(), arg);
+ } else if (arg instanceof Date) {
+ // format a Date if can
+ dest.formatAndAppend(getStockDateFormatter(), arg);
+ } else {
+ dest.append(arg.toString());
+ }
+ } else if(argType==ArgType.CHOICE) {
+ if (!(arg instanceof Number)) {
+ throw new IllegalArgumentException("'" + arg + "' is not a Number");
+ }
+ double number = ((Number)arg).doubleValue();
+ int subMsgStart=findChoiceSubMessage(msgPattern, i, number);
+ formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
+ } else if(argType.hasPluralStyle()) {
+ if (!(arg instanceof Number)) {
+ throw new IllegalArgumentException("'" + arg + "' is not a Number");
+ }
+ PluralSelectorProvider selector;
+ if(argType == ArgType.PLURAL) {
+ if (pluralProvider == null) {
+ pluralProvider = new PluralSelectorProvider(this, PluralType.CARDINAL);
+ }
+ selector = pluralProvider;
+ } else {
+ if (ordinalProvider == null) {
+ ordinalProvider = new PluralSelectorProvider(this, PluralType.ORDINAL);
+ }
+ selector = ordinalProvider;
+ }
+ Number number = (Number)arg;
+ double offset=msgPattern.getPluralOffset(i);
+ PluralSelectorContext context =
+ new PluralSelectorContext(i, argName, number, offset);
+ int subMsgStart=PluralFormat.findSubMessage(
+ msgPattern, i, selector, context, number.doubleValue());
+ formatComplexSubMessage(subMsgStart, context, args, argsMap, dest);
+ } else if(argType==ArgType.SELECT) {
+ int subMsgStart=SelectFormat.findSubMessage(msgPattern, i, arg.toString());
+ formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
+ } else {
+ // This should never happen.
+ throw new IllegalStateException("unexpected argType "+argType);
+ }
+ fp = updateMetaData(dest, prevDestLength, fp, argId);
+ prevIndex=msgPattern.getPart(argLimit).getLimit();
+ i=argLimit;
+ }
+ }
+
+ private void formatComplexSubMessage(
+ int msgStart, PluralSelectorContext pluralNumber,
+ Object[] args, Map<String, Object> argsMap,
+ AppendableWrapper dest) {
+ if (!msgPattern.jdkAposMode()) {
+ format(msgStart, pluralNumber, args, argsMap, dest, null);
+ return;
+ }
+ // JDK compatibility mode: (see JDK MessageFormat.format() API docs)
+ // - remove SKIP_SYNTAX; that is, remove half of the apostrophes
+ // - if the result string contains an open curly brace '{' then
+ // instantiate a temporary MessageFormat object and format again;
+ // otherwise just append the result string
+ String msgString = msgPattern.getPatternString();
+ String subMsgString;
+ StringBuilder sb = null;
+ int prevIndex = msgPattern.getPart(msgStart).getLimit();
+ for (int i = msgStart;;) {
+ Part part = msgPattern.getPart(++i);
+ Part.Type type = part.getType();
+ int index = part.getIndex();
+ if (type == Part.Type.MSG_LIMIT) {
+ if (sb == null) {
+ subMsgString = msgString.substring(prevIndex, index);
+ } else {
+ subMsgString = sb.append(msgString, prevIndex, index).toString();
+ }
+ break;
+ } else if (type == Part.Type.REPLACE_NUMBER || type == Part.Type.SKIP_SYNTAX) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(msgString, prevIndex, index);
+ if (type == Part.Type.REPLACE_NUMBER) {
+ if(pluralNumber.forReplaceNumber) {
+ // number-offset was already formatted.
+ sb.append(pluralNumber.numberString);
+ } else {
+ sb.append(getStockNumberFormatter().format(pluralNumber.number));
+ }
+ }
+ prevIndex = part.getLimit();
+ } else if (type == Part.Type.ARG_START) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ sb.append(msgString, prevIndex, index);
+ prevIndex = index;
+ i = msgPattern.getLimitPartIndex(i);
+ index = msgPattern.getPart(i).getLimit();
+ MessagePattern.appendReducedApostrophes(msgString, prevIndex, index, sb);
+ prevIndex = index;
+ }
+ }
+ if (subMsgString.indexOf('{') >= 0) {
+ MessageFormat subMsgFormat = new MessageFormat("", ulocale);
+ subMsgFormat.applyPattern(subMsgString, MessagePattern.ApostropheMode.DOUBLE_REQUIRED);
+ subMsgFormat.format(0, null, args, argsMap, dest, null);
+ } else {
+ dest.append(subMsgString);
+ }
+ }
+
+ /**
+ * Read as much literal string from the pattern string as possible. This stops
+ * as soon as it finds an argument, or it reaches the end of the string.
+ * @param from Index in the pattern string to start from.
+ * @return A substring from the pattern string representing the longest possible
+ * substring with no arguments.
+ */
+ private String getLiteralStringUntilNextArgument(int from) {
+ StringBuilder b = new StringBuilder();
+ String msgString=msgPattern.getPatternString();
+ int prevIndex=msgPattern.getPart(from).getLimit();
+ for(int i=from+1;; ++i) {
+ Part part=msgPattern.getPart(i);
+ Part.Type type=part.getType();
+ int index=part.getIndex();
+ b.append(msgString, prevIndex, index);
+ if(type==Part.Type.ARG_START || type==Part.Type.MSG_LIMIT) {
+ return b.toString();
+ }
+ assert type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR :
+ "Unexpected Part "+part+" in parsed message.";
+ prevIndex=part.getLimit();
+ }
+ }
+
+ private FieldPosition updateMetaData(AppendableWrapper dest, int prevLength,
+ FieldPosition fp, Object argId) {
+ if (dest.attributes != null && prevLength < dest.length) {
+ dest.attributes.add(new AttributeAndPosition(argId, prevLength, dest.length));
+ }
+ if (fp != null && Field.ARGUMENT.equals(fp.getFieldAttribute())) {
+ fp.setBeginIndex(prevLength);
+ fp.setEndIndex(dest.length);
+ return null;
+ }
+ return fp;
+ }
+
+ // This lives here because ICU4J does not have its own ChoiceFormat class.
+ /**
+ * Finds the ChoiceFormat sub-message for the given number.
+ * @param pattern A MessagePattern.
+ * @param partIndex the index of the first ChoiceFormat argument style part.
+ * @param number a number to be mapped to one of the ChoiceFormat argument's intervals
+ * @return the sub-message start part index.
+ */
+ private static int findChoiceSubMessage(MessagePattern pattern, int partIndex, double number) {
+ int count=pattern.countParts();
+ int msgStart;
+ // Iterate over (ARG_INT|DOUBLE, ARG_SELECTOR, message) tuples
+ // until ARG_LIMIT or end of choice-only pattern.
+ // Ignore the first number and selector and start the loop on the first message.
+ partIndex+=2;
+ for(;;) {
+ // Skip but remember the current sub-message.
+ msgStart=partIndex;
+ partIndex=pattern.getLimitPartIndex(partIndex);
+ if(++partIndex>=count) {
+ // Reached the end of the choice-only pattern.
+ // Return with the last sub-message.
+ break;
+ }
+ Part part=pattern.getPart(partIndex++);
+ Part.Type type=part.getType();
+ if(type==Part.Type.ARG_LIMIT) {
+ // Reached the end of the ChoiceFormat style.
+ // Return with the last sub-message.
+ break;
+ }
+ // part is an ARG_INT or ARG_DOUBLE
+ assert type.hasNumericValue();
+ double boundary=pattern.getNumericValue(part);
+ // Fetch the ARG_SELECTOR character.
+ int selectorIndex=pattern.getPatternIndex(partIndex++);
+ char boundaryChar=pattern.getPatternString().charAt(selectorIndex);
+ if(boundaryChar=='<' ? !(number>boundary) : !(number>=boundary)) {
+ // The number is in the interval between the previous boundary and the current one.
+ // Return with the sub-message between them.
+ // The !(a>b) and !(a>=b) comparisons are equivalent to
+ // (a<=b) and (a<b) except they "catch" NaN.
+ break;
+ }
+ }
+ return msgStart;
+ }
+
+ // Ported from C++ ChoiceFormat::parse().
+ private static double parseChoiceArgument(
+ MessagePattern pattern, int partIndex,
+ String source, ParsePosition pos) {
+ // find the best number (defined as the one with the longest parse)
+ int start = pos.getIndex();
+ int furthest = start;
+ double bestNumber = Double.NaN;
+ double tempNumber = 0.0;
+ while (pattern.getPartType(partIndex) != Part.Type.ARG_LIMIT) {
+ tempNumber = pattern.getNumericValue(pattern.getPart(partIndex));
+ partIndex += 2; // skip the numeric part and ignore the ARG_SELECTOR
+ int msgLimit = pattern.getLimitPartIndex(partIndex);
+ int len = matchStringUntilLimitPart(pattern, partIndex, msgLimit, source, start);
+ if (len >= 0) {
+ int newIndex = start + len;
+ if (newIndex > furthest) {
+ furthest = newIndex;
+ bestNumber = tempNumber;
+ if (furthest == source.length()) {
+ break;
+ }
+ }
+ }
+ partIndex = msgLimit + 1;
+ }
+ if (furthest == start) {
+ pos.setErrorIndex(start);
+ } else {
+ pos.setIndex(furthest);
+ }
+ return bestNumber;
+ }
+
+ /**
+ * Matches the pattern string from the end of the partIndex to
+ * the beginning of the limitPartIndex,
+ * including all syntax except SKIP_SYNTAX,
+ * against the source string starting at sourceOffset.
+ * If they match, returns the length of the source string match.
+ * Otherwise returns -1.
+ */
+ private static int matchStringUntilLimitPart(
+ MessagePattern pattern, int partIndex, int limitPartIndex,
+ String source, int sourceOffset) {
+ int matchingSourceLength = 0;
+ String msgString = pattern.getPatternString();
+ int prevIndex = pattern.getPart(partIndex).getLimit();
+ for (;;) {
+ Part part = pattern.getPart(++partIndex);
+ if (partIndex == limitPartIndex || part.getType() == Part.Type.SKIP_SYNTAX) {
+ int index = part.getIndex();
+ int length = index - prevIndex;
+ if (length != 0 && !source.regionMatches(sourceOffset, msgString, prevIndex, length)) {
+ return -1; // mismatch
+ }
+ matchingSourceLength += length;
+ if (partIndex == limitPartIndex) {
+ return matchingSourceLength;
+ }
+ prevIndex = part.getLimit(); // SKIP_SYNTAX
+ }
+ }
+ }
+
+ /**
+ * Finds the "other" sub-message.
+ * @param partIndex the index of the first PluralFormat argument style part.
+ * @return the "other" sub-message start part index.
+ */
+ private int findOtherSubMessage(int partIndex) {
+ int count=msgPattern.countParts();
+ MessagePattern.Part part=msgPattern.getPart(partIndex);
+ if(part.getType().hasNumericValue()) {
+ ++partIndex;
+ }
+ // Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples
+ // until ARG_LIMIT or end of plural-only pattern.
+ do {
+ part=msgPattern.getPart(partIndex++);
+ MessagePattern.Part.Type type=part.getType();
+ if(type==MessagePattern.Part.Type.ARG_LIMIT) {
+ break;
+ }
+ assert type==MessagePattern.Part.Type.ARG_SELECTOR;
+ // part is an ARG_SELECTOR followed by an optional explicit value, and then a message
+ if(msgPattern.partSubstringMatches(part, "other")) {
+ return partIndex;
+ }
+ if(msgPattern.getPartType(partIndex).hasNumericValue()) {
+ ++partIndex; // skip the numeric-value part of "=1" etc.
+ }
+ partIndex=msgPattern.getLimitPartIndex(partIndex);
+ } while(++partIndex<count);
+ return 0;
+ }
+
+ /**
+ * Returns the ARG_START index of the first occurrence of the plural number in a sub-message.
+ * Returns -1 if it is a REPLACE_NUMBER.
+ * Returns 0 if there is neither.
+ */
+ private int findFirstPluralNumberArg(int msgStart, String argName) {
+ for(int i=msgStart+1;; ++i) {
+ Part part=msgPattern.getPart(i);
+ Part.Type type=part.getType();
+ if(type==Part.Type.MSG_LIMIT) {
+ return 0;
+ }
+ if(type==Part.Type.REPLACE_NUMBER) {
+ return -1;
+ }
+ if(type==Part.Type.ARG_START) {
+ ArgType argType=part.getArgType();
+ if(argName.length()!=0 && (argType==ArgType.NONE || argType==ArgType.SIMPLE)) {
+ part=msgPattern.getPart(i+1); // ARG_NUMBER or ARG_NAME
+ if(msgPattern.partSubstringMatches(part, argName)) {
+ return i;
+ }
+ }
+ i=msgPattern.getLimitPartIndex(i);
+ }
+ }
+ }
+
+ /**
+ * Mutable input/output values for the PluralSelectorProvider.
+ * Separate so that it is possible to make MessageFormat Freezable.
+ */
+ private static final class PluralSelectorContext {
+ private PluralSelectorContext(int start, String name, Number num, double off) {
+ startIndex = start;
+ argName = name;
+ // number needs to be set even when select() is not called.
+ // Keep it as a Number/Formattable:
+ // For format() methods, and to preserve information (e.g., BigDecimal).
+ if(off == 0) {
+ number = num;
+ } else {
+ number = num.doubleValue() - off;
+ }
+ offset = off;
+ }
+ @Override
+ public String toString() {
+ throw new AssertionError("PluralSelectorContext being formatted, rather than its number");
+ }
+
+ // Input values for plural selection with decimals.
+ int startIndex;
+ String argName;
+ /** argument number - plural offset */
+ Number number;
+ double offset;
+ // Output values for plural selection with decimals.
+ /** -1 if REPLACE_NUMBER, 0 arg not found, >0 ARG_START index */
+ int numberArgIndex;
+ Format formatter;
+ /** formatted argument number - plural offset */
+ String numberString;
+ /** true if number-offset was formatted with the stock number formatter */
+ boolean forReplaceNumber;
+ }
+
+ /**
+ * This provider helps defer instantiation of a PluralRules object
+ * until we actually need to select a keyword.
+ * For example, if the number matches an explicit-value selector like "=1"
+ * we do not need any PluralRules.
+ */
+ private static final class PluralSelectorProvider implements PluralFormat.PluralSelector {
+ public PluralSelectorProvider(MessageFormat mf, PluralType type) {
+ msgFormat = mf;
+ this.type = type;
+ }
+ public String select(Object ctx, double number) {
+ if(rules == null) {
+ rules = PluralRules.forLocale(msgFormat.ulocale, type);
+ }
+ // Select a sub-message according to how the number is formatted,
+ // which is specified in the selected sub-message.
+ // We avoid this circle by looking at how
+ // the number is formatted in the "other" sub-message
+ // which must always be present and usually contains the number.
+ // Message authors should be consistent across sub-messages.
+ PluralSelectorContext context = (PluralSelectorContext)ctx;
+ int otherIndex = msgFormat.findOtherSubMessage(context.startIndex);
+ context.numberArgIndex = msgFormat.findFirstPluralNumberArg(otherIndex, context.argName);
+ if(context.numberArgIndex > 0 && msgFormat.cachedFormatters != null) {
+ context.formatter = msgFormat.cachedFormatters.get(context.numberArgIndex);
+ }
+ if(context.formatter == null) {
+ context.formatter = msgFormat.getStockNumberFormatter();
+ context.forReplaceNumber = true;
+ }
+ assert context.number.doubleValue() == number; // argument number minus the offset
+ context.numberString = context.formatter.format(context.number);
+ if(context.formatter instanceof DecimalFormat) {
+ FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
+ return rules.select(dec);
+ } else {
+ return rules.select(number);
+ }
+ }
+ private MessageFormat msgFormat;
+ private PluralRules rules;
+ private PluralType type;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void format(Object arguments, AppendableWrapper result, FieldPosition fp) {
+ if ((arguments == null || arguments instanceof Map)) {
+ format(null, (Map<String, Object>)arguments, result, fp);
+ } else {
+ format((Object[])arguments, null, result, fp);
+ }
+ }
+
+ /**
+ * Internal routine used by format.
+ *
+ * @throws IllegalArgumentException if an argument in the
+ * <code>arguments</code> map is not of the type
+ * expected by the format element(s) that use it.
+ */
+ private void format(Object[] arguments, Map<String, Object> argsMap,
+ AppendableWrapper dest, FieldPosition fp) {
+ if (arguments != null && msgPattern.hasNamedArguments()) {
+ throw new IllegalArgumentException(
+ "This method is not available in MessageFormat objects " +
+ "that use alphanumeric argument names.");
+ }
+ format(0, null, arguments, argsMap, dest, fp);
+ }
+
+ private void resetPattern() {
+ if (msgPattern != null) {
+ msgPattern.clear();
+ }
+ if (cachedFormatters != null) {
+ cachedFormatters.clear();
+ }
+ customFormatArgStarts = null;
+ }
+
+ private static final String[] typeList =
+ { "number", "date", "time", "spellout", "ordinal", "duration" };
+ private static final int
+ TYPE_NUMBER = 0,
+ TYPE_DATE = 1,
+ TYPE_TIME = 2,
+ TYPE_SPELLOUT = 3,
+ TYPE_ORDINAL = 4,
+ TYPE_DURATION = 5;
+
+ private static final String[] modifierList =
+ {"", "currency", "percent", "integer"};
+
+ private static final int
+ MODIFIER_EMPTY = 0,
+ MODIFIER_CURRENCY = 1,
+ MODIFIER_PERCENT = 2,
+ MODIFIER_INTEGER = 3;
+
+ private static final String[] dateModifierList =
+ {"", "short", "medium", "long", "full"};
+
+ private static final int
+ DATE_MODIFIER_EMPTY = 0,
+ DATE_MODIFIER_SHORT = 1,
+ DATE_MODIFIER_MEDIUM = 2,
+ DATE_MODIFIER_LONG = 3,
+ DATE_MODIFIER_FULL = 4;
+
+ // Creates an appropriate Format object for the type and style passed.
+ // Both arguments cannot be null.
+ private Format createAppropriateFormat(String type, String style) {
+ Format newFormat = null;
+ int subformatType = findKeyword(type, typeList);
+ switch (subformatType){
+ case TYPE_NUMBER:
+ switch (findKeyword(style, modifierList)) {
+ case MODIFIER_EMPTY:
+ newFormat = NumberFormat.getInstance(ulocale);
+ break;
+ case MODIFIER_CURRENCY:
+ newFormat = NumberFormat.getCurrencyInstance(ulocale);
+ break;
+ case MODIFIER_PERCENT:
+ newFormat = NumberFormat.getPercentInstance(ulocale);
+ break;
+ case MODIFIER_INTEGER:
+ newFormat = NumberFormat.getIntegerInstance(ulocale);
+ break;
+ default: // pattern
+ newFormat = new DecimalFormat(style,
+ new DecimalFormatSymbols(ulocale));
+ break;
+ }
+ break;
+ case TYPE_DATE:
+ switch (findKeyword(style, dateModifierList)) {
+ case DATE_MODIFIER_EMPTY:
+ newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
+ break;
+ case DATE_MODIFIER_SHORT:
+ newFormat = DateFormat.getDateInstance(DateFormat.SHORT, ulocale);
+ break;
+ case DATE_MODIFIER_MEDIUM:
+ newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
+ break;
+ case DATE_MODIFIER_LONG:
+ newFormat = DateFormat.getDateInstance(DateFormat.LONG, ulocale);
+ break;
+ case DATE_MODIFIER_FULL:
+ newFormat = DateFormat.getDateInstance(DateFormat.FULL, ulocale);
+ break;
+ default:
+ newFormat = new SimpleDateFormat(style, ulocale);
+ break;
+ }
+ break;
+ case TYPE_TIME:
+ switch (findKeyword(style, dateModifierList)) {
+ case DATE_MODIFIER_EMPTY:
+ newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
+ break;
+ case DATE_MODIFIER_SHORT:
+ newFormat = DateFormat.getTimeInstance(DateFormat.SHORT, ulocale);
+ break;
+ case DATE_MODIFIER_MEDIUM:
+ newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
+ break;
+ case DATE_MODIFIER_LONG:
+ newFormat = DateFormat.getTimeInstance(DateFormat.LONG, ulocale);
+ break;
+ case DATE_MODIFIER_FULL:
+ newFormat = DateFormat.getTimeInstance(DateFormat.FULL, ulocale);
+ break;
+ default:
+ newFormat = new SimpleDateFormat(style, ulocale);
+ break;
+ }
+ break;
+ case TYPE_SPELLOUT:
+ {
+ RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
+ RuleBasedNumberFormat.SPELLOUT);
+ String ruleset = style.trim();
+ if (ruleset.length() != 0) {
+ try {
+ rbnf.setDefaultRuleSet(ruleset);
+ }
+ catch (Exception e) {
+ // warn invalid ruleset
+ }
+ }
+ newFormat = rbnf;
+ }
+ break;
+ case TYPE_ORDINAL:
+ {
+ RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
+ RuleBasedNumberFormat.ORDINAL);
+ String ruleset = style.trim();
+ if (ruleset.length() != 0) {
+ try {
+ rbnf.setDefaultRuleSet(ruleset);
+ }
+ catch (Exception e) {
+ // warn invalid ruleset
+ }
+ }
+ newFormat = rbnf;
+ }
+ break;
+ case TYPE_DURATION:
+ {
+ RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
+ RuleBasedNumberFormat.DURATION);
+ String ruleset = style.trim();
+ if (ruleset.length() != 0) {
+ try {
+ rbnf.setDefaultRuleSet(ruleset);
+ }
+ catch (Exception e) {
+ // warn invalid ruleset
+ }
+ }
+ newFormat = rbnf;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown format type \"" + type + "\"");
+ }
+ return newFormat;
+ }
+
+ private static final Locale rootLocale = new Locale(""); // Locale.ROOT only @since 1.6
+
+ private static final int findKeyword(String s, String[] list) {
+ s = PatternProps.trimWhiteSpace(s).toLowerCase(rootLocale);
+ for (int i = 0; i < list.length; ++i) {
+ if (s.equals(list[i]))
+ return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Custom serialization, new in ICU 4.8.
+ * We do not want to use default serialization because we only have a small
+ * amount of persistent state which is better expressed explicitly
+ * rather than via writing field objects.
+ * @param out The output stream.
+ * @serialData Writes the locale as a BCP 47 language tag string,
+ * the MessagePattern.ApostropheMode as an object,
+ * and the pattern string (null if none was applied).
+ * Followed by an int with the number of (int formatIndex, Object formatter) pairs,
+ * and that many such pairs, corresponding to previous setFormat() calls for custom formats.
+ * Followed by an int with the number of (int, Object) pairs,
+ * and that many such pairs, for future (post-ICU 4.8) extension of the serialization format.
+ */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ // ICU 4.8 custom serialization.
+ // locale as a BCP 47 language tag
+ out.writeObject(ulocale.toLanguageTag());
+ // ApostropheMode
+ if (msgPattern == null) {
+ msgPattern = new MessagePattern();
+ }
+ out.writeObject(msgPattern.getApostropheMode());
+ // message pattern string
+ out.writeObject(msgPattern.getPatternString());
+ // custom formatters
+ if (customFormatArgStarts == null || customFormatArgStarts.isEmpty()) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(customFormatArgStarts.size());
+ int formatIndex = 0;
+ for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
+ if (customFormatArgStarts.contains(partIndex)) {
+ out.writeInt(formatIndex);
+ out.writeObject(cachedFormatters.get(partIndex));
+ }
+ ++formatIndex;
+ }
+ }
+ // number of future (int, Object) pairs
+ out.writeInt(0);
+ }
+
+ /**
+ * Custom deserialization, new in ICU 4.8. See comments on writeObject().
+ * @throws InvalidObjectException if the objects read from the stream is invalid.
+ */
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ // ICU 4.8 custom deserialization.
+ String languageTag = (String)in.readObject();
+ ulocale = ULocale.forLanguageTag(languageTag);
+ MessagePattern.ApostropheMode aposMode = (MessagePattern.ApostropheMode)in.readObject();
+ if (msgPattern == null || aposMode != msgPattern.getApostropheMode()) {
+ msgPattern = new MessagePattern(aposMode);
+ }
+ String msg = (String)in.readObject();
+ if (msg != null) {
+ applyPattern(msg);
+ }
+ // custom formatters
+ for (int numFormatters = in.readInt(); numFormatters > 0; --numFormatters) {
+ int formatIndex = in.readInt();
+ Format formatter = (Format)in.readObject();
+ setFormat(formatIndex, formatter);
+ }
+ // skip future (int, Object) pairs
+ for (int numPairs = in.readInt(); numPairs > 0; --numPairs) {
+ in.readInt();
+ in.readObject();
+ }
+ }
+
+ private void cacheExplicitFormats() {
+ if (cachedFormatters != null) {
+ cachedFormatters.clear();
+ }
+ customFormatArgStarts = null;
+ // The last two "parts" can at most be ARG_LIMIT and MSG_LIMIT
+ // which we need not examine.
+ int limit = msgPattern.countParts() - 2;
+ // This loop starts at part index 1 because we do need to examine
+ // ARG_START parts. (But we can ignore the MSG_START.)
+ for(int i=1; i < limit; ++i) {
+ Part part = msgPattern.getPart(i);
+ if(part.getType()!=Part.Type.ARG_START) {
+ continue;
+ }
+ ArgType argType=part.getArgType();
+ if(argType != ArgType.SIMPLE) {
+ continue;
+ }
+ int index = i;
+ i += 2;
+ String explicitType = msgPattern.getSubstring(msgPattern.getPart(i++));
+ String style = "";
+ if ((part = msgPattern.getPart(i)).getType() == MessagePattern.Part.Type.ARG_STYLE) {
+ style = msgPattern.getSubstring(part);
+ ++i;
+ }
+ Format formatter = createAppropriateFormat(explicitType, style);
+ setArgStartFormat(index, formatter);
+ }
+ }
+
+ /**
+ * Sets a formatter for a MessagePattern ARG_START part index.
+ */
+ private void setArgStartFormat(int argStart, Format formatter) {
+ if (cachedFormatters == null) {
+ cachedFormatters = new HashMap<Integer, Format>();
+ }
+ cachedFormatters.put(argStart, formatter);
+ }
+
+ /**
+ * Sets a custom formatter for a MessagePattern ARG_START part index.
+ * "Custom" formatters are provided by the user via setFormat() or similar APIs.
+ */
+ private void setCustomArgStartFormat(int argStart, Format formatter) {
+ setArgStartFormat(argStart, formatter);
+ if (customFormatArgStarts == null) {
+ customFormatArgStarts = new HashSet<Integer>();
+ }
+ customFormatArgStarts.add(argStart);
+ }
+
+ private static final char SINGLE_QUOTE = '\'';
+ private static final char CURLY_BRACE_LEFT = '{';
+ private static final char CURLY_BRACE_RIGHT = '}';
+
+ private static final int STATE_INITIAL = 0;
+ private static final int STATE_SINGLE_QUOTE = 1;
+ private static final int STATE_IN_QUOTE = 2;
+ private static final int STATE_MSG_ELEMENT = 3;
+
+ /**
+ * {@icu} Converts an 'apostrophe-friendly' pattern into a standard
+ * pattern.
+ * <em>This is obsolete for ICU 4.8 and higher MessageFormat pattern strings.</em>
+ * It can still be useful together with the JDK MessageFormat.
+ *
+ * <p>See the class description for more about apostrophes and quoting,
+ * and differences between ICU and the JDK.
+ *
+ * <p>The JDK MessageFormat and ICU 4.6 and earlier MessageFormat
+ * treat all ASCII apostrophes as
+ * quotes, which is problematic in some languages, e.g.
+ * French, where apostrophe is commonly used. This utility
+ * assumes that only an unpaired apostrophe immediately before
+ * a brace is a true quote. Other unpaired apostrophes are paired,
+ * and the resulting standard pattern string is returned.
+ *
+ * <p><b>Note</b>: It is not guaranteed that the returned pattern
+ * is indeed a valid pattern. The only effect is to convert
+ * between patterns having different quoting semantics.
+ *
+ * <p><b>Note</b>: This method only works on top-level messageText,
+ * not messageText nested inside a complexArg.
+ *
+ * @param pattern the 'apostrophe-friendly' pattern to convert
+ * @return the standard equivalent of the original pattern
+ * @stable ICU 3.4
+ */
+ public static String autoQuoteApostrophe(String pattern) {
+ StringBuilder buf = new StringBuilder(pattern.length() * 2);
+ int state = STATE_INITIAL;
+ int braceCount = 0;
+ for (int i = 0, j = pattern.length(); i < j; ++i) {
+ char c = pattern.charAt(i);
+ switch (state) {
+ case STATE_INITIAL:
+ switch (c) {
+ case SINGLE_QUOTE:
+ state = STATE_SINGLE_QUOTE;
+ break;
+ case CURLY_BRACE_LEFT:
+ state = STATE_MSG_ELEMENT;
+ ++braceCount;
+ break;
+ }
+ break;
+ case STATE_SINGLE_QUOTE:
+ switch (c) {
+ case SINGLE_QUOTE:
+ state = STATE_INITIAL;
+ break;
+ case CURLY_BRACE_LEFT:
+ case CURLY_BRACE_RIGHT:
+ state = STATE_IN_QUOTE;
+ break;
+ default:
+ buf.append(SINGLE_QUOTE);
+ state = STATE_INITIAL;
+ break;
+ }
+ break;
+ case STATE_IN_QUOTE:
+ switch (c) {
+ case SINGLE_QUOTE:
+ state = STATE_INITIAL;
+ break;
+ }
+ break;
+ case STATE_MSG_ELEMENT:
+ switch (c) {
+ case CURLY_BRACE_LEFT:
+ ++braceCount;
+ break;
+ case CURLY_BRACE_RIGHT:
+ if (--braceCount == 0) {
+ state = STATE_INITIAL;
+ }
+ break;
+ }
+ break;
+ ///CLOVER:OFF
+ default: // Never happens.
+ break;
+ ///CLOVER:ON
+ }
+ buf.append(c);
+ }
+ // End of scan
+ if (state == STATE_SINGLE_QUOTE || state == STATE_IN_QUOTE) {
+ buf.append(SINGLE_QUOTE);
+ }
+ return new String(buf);
+ }
+
+ /**
+ * Convenience wrapper for Appendable, tracks the result string length.
+ * Also, Appendable throws IOException, and we turn that into a RuntimeException
+ * so that we need no throws clauses.
+ */
+ private static final class AppendableWrapper {
+ public AppendableWrapper(StringBuilder sb) {
+ app = sb;
+ length = sb.length();
+ attributes = null;
+ }
+
+ public AppendableWrapper(StringBuffer sb) {
+ app = sb;
+ length = sb.length();
+ attributes = null;
+ }
+
+ public void useAttributes() {
+ attributes = new ArrayList<AttributeAndPosition>();
+ }
+
+ public void append(CharSequence s) {
+ try {
+ app.append(s);
+ length += s.length();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(CharSequence s, int start, int limit) {
+ try {
+ app.append(s, start, limit);
+ length += limit - start;
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void append(CharacterIterator iterator) {
+ length += append(app, iterator);
+ }
+
+ public static int append(Appendable result, CharacterIterator iterator) {
+ try {
+ int start = iterator.getBeginIndex();
+ int limit = iterator.getEndIndex();
+ int length = limit - start;
+ if (start < limit) {
+ result.append(iterator.first());
+ while (++start < limit) {
+ result.append(iterator.next());
+ }
+ }
+ return length;
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void formatAndAppend(Format formatter, Object arg) {
+ if (attributes == null) {
+ append(formatter.format(arg));
+ } else {
+ AttributedCharacterIterator formattedArg = formatter.formatToCharacterIterator(arg);
+ int prevLength = length;
+ append(formattedArg);
+ // Copy all of the attributes from formattedArg to our attributes list.
+ formattedArg.first();
+ int start = formattedArg.getIndex(); // Should be 0 but might not be.
+ int limit = formattedArg.getEndIndex(); // == start + length - prevLength
+ int offset = prevLength - start; // Adjust attribute indexes for the result string.
+ while (start < limit) {
+ Map<Attribute, Object> map = formattedArg.getAttributes();
+ int runLimit = formattedArg.getRunLimit();
+ if (map.size() != 0) {
+ for (Map.Entry<Attribute, Object> entry : map.entrySet()) {
+ attributes.add(
+ new AttributeAndPosition(
+ entry.getKey(), entry.getValue(),
+ offset + start, offset + runLimit));
+ }
+ }
+ start = runLimit;
+ formattedArg.setIndex(start);
+ }
+ }
+ }
+
+ public void formatAndAppend(Format formatter, Object arg, String argString) {
+ if (attributes == null && argString != null) {
+ append(argString);
+ } else {
+ formatAndAppend(formatter, arg);
+ }
+ }
+
+ private Appendable app;
+ private int length;
+ private List<AttributeAndPosition> attributes;
+ }
+
+ private static final class AttributeAndPosition {
+ /**
+ * Defaults the field to Field.ARGUMENT.
+ */
+ public AttributeAndPosition(Object fieldValue, int startIndex, int limitIndex) {
+ init(Field.ARGUMENT, fieldValue, startIndex, limitIndex);
+ }
+
+ public AttributeAndPosition(Attribute field, Object fieldValue, int startIndex, int limitIndex) {
+ init(field, fieldValue, startIndex, limitIndex);
+ }
+
+ public void init(Attribute field, Object fieldValue, int startIndex, int limitIndex) {
+ key = field;
+ value = fieldValue;
+ start = startIndex;
+ limit = limitIndex;
+ }
+
+ private Attribute key;
+ private Object value;
+ private int start;
+ private int limit;
+ }
+}
diff --git a/src/main/com/android/i18n/MessagePattern.java b/src/main/com/android/i18n/MessagePattern.java
new file mode 100644
index 0000000..bcb84b1
--- /dev/null
+++ b/src/main/com/android/i18n/MessagePattern.java
@@ -0,0 +1,1612 @@
+/*
+*******************************************************************************
+* Copyright (C) 2010-2013, International Business Machines
+* Corporation and others. All Rights Reserved.
+*******************************************************************************
+* created on: 2010aug21
+* created by: Markus W. Scherer
+*/
+
+package com.ibm.icu.text;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+import com.ibm.icu.impl.ICUConfig;
+import com.ibm.icu.impl.PatternProps;
+import com.ibm.icu.util.Freezable;
+
+//Note: Minimize ICU dependencies, only use a very small part of the ICU core.
+//In particular, do not depend on *Format classes.
+
+/**
+ * Parses and represents ICU MessageFormat patterns.
+ * Also handles patterns for ChoiceFormat, PluralFormat and SelectFormat.
+ * Used in the implementations of those classes as well as in tools
+ * for message validation, translation and format conversion.
+ * <p>
+ * The parser handles all syntax relevant for identifying message arguments.
+ * This includes "complex" arguments whose style strings contain
+ * nested MessageFormat pattern substrings.
+ * For "simple" arguments (with no nested MessageFormat pattern substrings),
+ * the argument style is not parsed any further.
+ * <p>
+ * The parser handles named and numbered message arguments and allows both in one message.
+ * <p>
+ * Once a pattern has been parsed successfully, iterate through the parsed data
+ * with countParts(), getPart() and related methods.
+ * <p>
+ * The data logically represents a parse tree, but is stored and accessed
+ * as a list of "parts" for fast and simple parsing and to minimize object allocations.
+ * Arguments and nested messages are best handled via recursion.
+ * For every _START "part", {@link #getLimitPartIndex(int)} efficiently returns
+ * the index of the corresponding _LIMIT "part".
+ * <p>
+ * List of "parts":
+ * <pre>
+ * message = MSG_START (SKIP_SYNTAX | INSERT_CHAR | REPLACE_NUMBER | argument)* MSG_LIMIT
+ * argument = noneArg | simpleArg | complexArg
+ * complexArg = choiceArg | pluralArg | selectArg
+ *
+ * noneArg = ARG_START.NONE (ARG_NAME | ARG_NUMBER) ARG_LIMIT.NONE
+ * simpleArg = ARG_START.SIMPLE (ARG_NAME | ARG_NUMBER) ARG_TYPE [ARG_STYLE] ARG_LIMIT.SIMPLE
+ * choiceArg = ARG_START.CHOICE (ARG_NAME | ARG_NUMBER) choiceStyle ARG_LIMIT.CHOICE
+ * pluralArg = ARG_START.PLURAL (ARG_NAME | ARG_NUMBER) pluralStyle ARG_LIMIT.PLURAL
+ * selectArg = ARG_START.SELECT (ARG_NAME | ARG_NUMBER) selectStyle ARG_LIMIT.SELECT
+ *
+ * choiceStyle = ((ARG_INT | ARG_DOUBLE) ARG_SELECTOR message)+
+ * pluralStyle = [ARG_INT | ARG_DOUBLE] (ARG_SELECTOR [ARG_INT | ARG_DOUBLE] message)+
+ * selectStyle = (ARG_SELECTOR message)+
+ * </pre>
+ * <ul>
+ * <li>Literal output text is not represented directly by "parts" but accessed
+ * between parts of a message, from one part's getLimit() to the next part's getIndex().
+ * <li><code>ARG_START.CHOICE</code> stands for an ARG_START Part with ArgType CHOICE.
+ * <li>In the choiceStyle, the ARG_SELECTOR has the '<', the '#' or
+ * the less-than-or-equal-to sign (U+2264).
+ * <li>In the pluralStyle, the first, optional numeric Part has the "offset:" value.
+ * The optional numeric Part between each (ARG_SELECTOR, message) pair
+ * is the value of an explicit-number selector like "=2",
+ * otherwise the selector is a non-numeric identifier.
+ * <li>The REPLACE_NUMBER Part can occur only in an immediate sub-message of the pluralStyle.
+ * <p>
+ * This class is not intended for public subclassing.
+ *
+ * @stable ICU 4.8
+ * @author Markus Scherer
+ */
+public final class MessagePattern implements Cloneable, Freezable<MessagePattern> {
+ /**
+ * Mode for when an apostrophe starts quoted literal text for MessageFormat output.
+ * The default is DOUBLE_OPTIONAL unless overridden via ICUConfig
+ * (/com/ibm/icu/ICUConfig.properties).
+ * <p>
+ * A pair of adjacent apostrophes always results in a single apostrophe in the output,
+ * even when the pair is between two single, text-quoting apostrophes.
+ * <p>
+ * The following table shows examples of desired MessageFormat.format() output
+ * with the pattern strings that yield that output.
+ * <p>
+ * <table>
+ * <tr>
+ * <th>Desired output</th>
+ * <th>DOUBLE_OPTIONAL</th>
+ * <th>DOUBLE_REQUIRED</th>
+ * </tr>
+ * <tr>
+ * <td>I see {many}</td>
+ * <td>I see '{many}'</td>
+ * <td>(same)</td>
+ * </tr>
+ * <tr>
+ * <td>I said {'Wow!'}</td>
+ * <td>I said '{''Wow!''}'</td>
+ * <td>(same)</td>
+ * </tr>
+ * <tr>
+ * <td>I don't know</td>
+ * <td>I don't know OR<br> I don''t know</td>
+ * <td>I don''t know</td>
+ * </tr>
+ * </table>
+ * @stable ICU 4.8
+ */
+ public enum ApostropheMode {
+ /**
+ * A literal apostrophe is represented by
+ * either a single or a double apostrophe pattern character.
+ * Within a MessageFormat pattern, a single apostrophe only starts quoted literal text
+ * if it immediately precedes a curly brace {},
+ * or a pipe symbol | if inside a choice format,
+ * or a pound symbol # if inside a plural format.
+ * <p>
+ * This is the default behavior starting with ICU 4.8.
+ * @stable ICU 4.8
+ */
+ DOUBLE_OPTIONAL,
+ /**
+ * A literal apostrophe must be represented by
+ * a double apostrophe pattern character.
+ * A single apostrophe always starts quoted literal text.
+ * <p>
+ * This is the behavior of ICU 4.6 and earlier, and of the JDK.
+ * @stable ICU 4.8
+ */
+ DOUBLE_REQUIRED
+ }
+
+ /**
+ * Constructs an empty MessagePattern with default ApostropheMode.
+ * @stable ICU 4.8
+ */
+ public MessagePattern() {
+ aposMode=defaultAposMode;
+ }
+
+ /**
+ * Constructs an empty MessagePattern.
+ * @param mode Explicit ApostropheMode.
+ * @stable ICU 4.8
+ */
+ public MessagePattern(ApostropheMode mode) {
+ aposMode=mode;
+ }
+
+ /**
+ * Constructs a MessagePattern with default ApostropheMode and
+ * parses the MessageFormat pattern string.
+ * @param pattern a MessageFormat pattern string
+ * @throws IllegalArgumentException for syntax errors in the pattern string
+ * @throws IndexOutOfBoundsException if certain limits are exceeded
+ * (e.g., argument number too high, argument name too long, etc.)
+ * @throws NumberFormatException if a number could not be parsed
+ * @stable ICU 4.8
+ */
+ public MessagePattern(String pattern) {
+ aposMode=defaultAposMode;
+ parse(pattern);
+ }
+
+ /**
+ * Parses a MessageFormat pattern string.
+ * @param pattern a MessageFormat pattern string
+ * @return this
+ * @throws IllegalArgumentException for syntax errors in the pattern string
+ * @throws IndexOutOfBoundsException if certain limits are exceeded
+ * (e.g., argument number too high, argument name too long, etc.)
+ * @throws NumberFormatException if a number could not be parsed
+ * @stable ICU 4.8
+ */
+ public MessagePattern parse(String pattern) {
+ preParse(pattern);
+ parseMessage(0, 0, 0, ArgType.NONE);
+ postParse();
+ return this;
+ }
+
+ /**
+ * Parses a ChoiceFormat pattern string.
+ * @param pattern a ChoiceFormat pattern string
+ * @return this
+ * @throws IllegalArgumentException for syntax errors in the pattern string
+ * @throws IndexOutOfBoundsException if certain limits are exceeded
+ * (e.g., argument number too high, argument name too long, etc.)
+ * @throws NumberFormatException if a number could not be parsed
+ * @stable ICU 4.8
+ */
+ public MessagePattern parseChoiceStyle(String pattern) {
+ preParse(pattern);
+ parseChoiceStyle(0, 0);
+ postParse();
+ return this;
+ }
+
+ /**
+ * Parses a PluralFormat pattern string.
+ * @param pattern a PluralFormat pattern string
+ * @return this
+ * @throws IllegalArgumentException for syntax errors in the pattern string
+ * @throws IndexOutOfBoundsException if certain limits are exceeded
+ * (e.g., argument number too high, argument name too long, etc.)
+ * @throws NumberFormatException if a number could not be parsed
+ * @stable ICU 4.8
+ */
+ public MessagePattern parsePluralStyle(String pattern) {
+ preParse(pattern);
+ parsePluralOrSelectStyle(ArgType.PLURAL, 0, 0);
+ postParse();
+ return this;
+ }
+
+ /**
+ * Parses a SelectFormat pattern string.
+ * @param pattern a SelectFormat pattern string
+ * @return this
+ * @throws IllegalArgumentException for syntax errors in the pattern string
+ * @throws IndexOutOfBoundsException if certain limits are exceeded
+ * (e.g., argument number too high, argument name too long, etc.)
+ * @throws NumberFormatException if a number could not be parsed
+ * @stable ICU 4.8
+ */
+ public MessagePattern parseSelectStyle(String pattern) {
+ preParse(pattern);
+ parsePluralOrSelectStyle(ArgType.SELECT, 0, 0);
+ postParse();
+ return this;
+ }
+
+ /**
+ * Clears this MessagePattern.
+ * countParts() will return 0.
+ * @stable ICU 4.8
+ */
+ public void clear() {
+ // Mostly the same as preParse().
+ if(isFrozen()) {
+ throw new UnsupportedOperationException(
+ "Attempt to clear() a frozen MessagePattern instance.");
+ }
+ msg=null;
+ hasArgNames=hasArgNumbers=false;
+ needsAutoQuoting=false;
+ parts.clear();
+ if(numericValues!=null) {
+ numericValues.clear();
+ }
+ }
+
+ /**
+ * Clears this MessagePattern and sets the ApostropheMode.
+ * countParts() will return 0.
+ * @param mode The new ApostropheMode.
+ * @stable ICU 4.8
+ */
+ public void clearPatternAndSetApostropheMode(ApostropheMode mode) {
+ clear();
+ aposMode=mode;
+ }
+
+ /**
+ * @param other another object to compare with.
+ * @return true if this object is equivalent to the other one.
+ * @stable ICU 4.8
+ */
+ @Override
+ public boolean equals(Object other) {
+ if(this==other) {
+ return true;
+ }
+ if(other==null || getClass()!=other.getClass()) {
+ return false;
+ }
+ MessagePattern o=(MessagePattern)other;
+ return
+ aposMode.equals(o.aposMode) &&
+ (msg==null ? o.msg==null : msg.equals(o.msg)) &&
+ parts.equals(o.parts);
+ // No need to compare numericValues if msg and parts are the same.
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 4.8
+ */
+ @Override
+ public int hashCode() {
+ return (aposMode.hashCode()*37+(msg!=null ? msg.hashCode() : 0))*37+parts.hashCode();
+ }
+
+ /**
+ * @return this instance's ApostropheMode.
+ * @stable ICU 4.8
+ */
+ public ApostropheMode getApostropheMode() {
+ return aposMode;
+ }
+
+ /**
+ * @return true if getApostropheMode() == ApostropheMode.DOUBLE_REQUIRED
+ * @internal
+ */
+ /* package */ boolean jdkAposMode() {
+ return aposMode == ApostropheMode.DOUBLE_REQUIRED;
+ }
+
+ /**
+ * @return the parsed pattern string (null if none was parsed).
+ * @stable ICU 4.8
+ */
+ public String getPatternString() {
+ return msg;
+ }
+
+ /**
+ * Does the parsed pattern have named arguments like {first_name}?
+ * @return true if the parsed pattern has at least one named argument.
+ * @stable ICU 4.8
+ */
+ public boolean hasNamedArguments() {
+ return hasArgNames;
+ }
+
+ /**
+ * Does the parsed pattern have numbered arguments like {2}?
+ * @return true if the parsed pattern has at least one numbered argument.
+ * @stable ICU 4.8
+ */
+ public boolean hasNumberedArguments() {
+ return hasArgNumbers;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 4.8
+ */
+ @Override
+ public String toString() {
+ return msg;
+ }
+
+ /**
+ * Validates and parses an argument name or argument number string.
+ * An argument name must be a "pattern identifier", that is, it must contain
+ * no Unicode Pattern_Syntax or Pattern_White_Space characters.
+ * If it only contains ASCII digits, then it must be a small integer with no leading zero.
+ * @param name Input string.
+ * @return &gt;=0 if the name is a valid number,
+ * ARG_NAME_NOT_NUMBER (-1) if it is a "pattern identifier" but not all ASCII digits,
+ * ARG_NAME_NOT_VALID (-2) if it is neither.
+ * @stable ICU 4.8
+ */
+ public static int validateArgumentName(String name) {
+ if(!PatternProps.isIdentifier(name)) {
+ return ARG_NAME_NOT_VALID;
+ }
+ return parseArgNumber(name, 0, name.length());
+ }
+
+ /**
+ * Return value from {@link #validateArgumentName(String)} for when
+ * the string is a valid "pattern identifier" but not a number.
+ * @stable ICU 4.8
+ */
+ public static final int ARG_NAME_NOT_NUMBER=-1;
+
+ /**
+ * Return value from {@link #validateArgumentName(String)} for when
+ * the string is invalid.
+ * It might not be a valid "pattern identifier",
+ * or it have only ASCII digits but there is a leading zero or the number is too large.
+ * @stable ICU 4.8
+ */
+ public static final int ARG_NAME_NOT_VALID=-2;
+
+ /**
+ * Returns a version of the parsed pattern string where each ASCII apostrophe
+ * is doubled (escaped) if it is not already, and if it is not interpreted as quoting syntax.
+ * <p>
+ * For example, this turns "I don't '{know}' {gender,select,female{h''er}other{h'im}}."
+ * into "I don''t '{know}' {gender,select,female{h''er}other{h''im}}."
+ * @return the deep-auto-quoted version of the parsed pattern string.
+ * @see MessageFormat#autoQuoteApostrophe(String)
+ * @stable ICU 4.8
+ */
+ public String autoQuoteApostropheDeep() {
+ if(!needsAutoQuoting) {
+ return msg;
+ }
+ StringBuilder modified=null;
+ // Iterate backward so that the insertion indexes do not change.
+ int count=countParts();
+ for(int i=count; i>0;) {
+ Part part;
+ if((part=getPart(--i)).getType()==Part.Type.INSERT_CHAR) {
+ if(modified==null) {
+ modified=new StringBuilder(msg.length()+10).append(msg);
+ }
+ modified.insert(part.index, (char)part.value);
+ }
+ }
+ if(modified==null) {
+ return msg;
+ } else {
+ return modified.toString();
+ }
+ }
+
+ /**
+ * Returns the number of "parts" created by parsing the pattern string.
+ * Returns 0 if no pattern has been parsed or clear() was called.
+ * @return the number of pattern parts.
+ * @stable ICU 4.8
+ */
+ public int countParts() {
+ return parts.size();
+ }
+
+ /**
+ * Gets the i-th pattern "part".
+ * @param i The index of the Part data. (0..countParts()-1)
+ * @return the i-th pattern "part".
+ * @throws IndexOutOfBoundsException if i is outside the (0..countParts()-1) range
+ * @stable ICU 4.8
+ */
+ public Part getPart(int i) {
+ return parts.get(i);
+ }
+
+ /**
+ * Returns the Part.Type of the i-th pattern "part".
+ * Convenience method for getPart(i).getType().
+ * @param i The index of the Part data. (0..countParts()-1)
+ * @return The Part.Type of the i-th Part.
+ * @throws IndexOutOfBoundsException if i is outside the (0..countParts()-1) range
+ * @stable ICU 4.8
+ */
+ public Part.Type getPartType(int i) {
+ return parts.get(i).type;
+ }
+
+ /**
+ * Returns the pattern index of the specified pattern "part".
+ * Convenience method for getPart(partIndex).getIndex().
+ * @param partIndex The index of the Part data. (0..countParts()-1)
+ * @return The pattern index of this Part.
+ * @throws IndexOutOfBoundsException if partIndex is outside the (0..countParts()-1) range
+ * @stable ICU 4.8
+ */
+ public int getPatternIndex(int partIndex) {
+ return parts.get(partIndex).index;
+ }
+
+ /**
+ * Returns the substring of the pattern string indicated by the Part.
+ * Convenience method for getPatternString().substring(part.getIndex(), part.getLimit()).
+ * @param part a part of this MessagePattern.
+ * @return the substring associated with part.
+ * @stable ICU 4.8
+ */
+ public String getSubstring(Part part) {
+ int index=part.index;
+ return msg.substring(index, index+part.length);
+ }
+
+ /**
+ * Compares the part's substring with the input string s.
+ * @param part a part of this MessagePattern.
+ * @param s a string.
+ * @return true if getSubstring(part).equals(s).
+ * @stable ICU 4.8
+ */
+ public boolean partSubstringMatches(Part part, String s) {
+ return msg.regionMatches(part.index, s, 0, part.length);
+ }
+
+ /**
+ * Returns the numeric value associated with an ARG_INT or ARG_DOUBLE.
+ * @param part a part of this MessagePattern.
+ * @return the part's numeric value, or NO_NUMERIC_VALUE if this is not a numeric part.
+ * @stable ICU 4.8
+ */
+ public double getNumericValue(Part part) {
+ Part.Type type=part.type;
+ if(type==Part.Type.ARG_INT) {
+ return part.value;
+ } else if(type==Part.Type.ARG_DOUBLE) {
+ return numericValues.get(part.value);
+ } else {
+ return NO_NUMERIC_VALUE;
+ }
+ }
+
+ /**
+ * Special value that is returned by getNumericValue(Part) when no
+ * numeric value is defined for a part.
+ * @see #getNumericValue
+ * @stable ICU 4.8
+ */
+ public static final double NO_NUMERIC_VALUE=-123456789;
+
+ /**
+ * Returns the "offset:" value of a PluralFormat argument, or 0 if none is specified.
+ * @param pluralStart the index of the first PluralFormat argument style part. (0..countParts()-1)
+ * @return the "offset:" value.
+ * @throws IndexOutOfBoundsException if pluralStart is outside the (0..countParts()-1) range
+ * @stable ICU 4.8
+ */
+ public double getPluralOffset(int pluralStart) {
+ Part part=parts.get(pluralStart);
+ if(part.type.hasNumericValue()) {
+ return getNumericValue(part);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the index of the ARG|MSG_LIMIT part corresponding to the ARG|MSG_START at start.
+ * @param start The index of some Part data (0..countParts()-1);
+ * this Part should be of Type ARG_START or MSG_START.
+ * @return The first i>start where getPart(i).getType()==ARG|MSG_LIMIT at the same nesting level,
+ * or start itself if getPartType(msgStart)!=ARG|MSG_START.
+ * @throws IndexOutOfBoundsException if start is outside the (0..countParts()-1) range
+ * @stable ICU 4.8
+ */
+ public int getLimitPartIndex(int start) {
+ int limit=parts.get(start).limitPartIndex;
+ if(limit<start) {
+ return start;
+ }
+ return limit;
+ }
+
+ /**
+ * A message pattern "part", representing a pattern parsing event.
+ * There is a part for the start and end of a message or argument,
+ * for quoting and escaping of and with ASCII apostrophes,
+ * and for syntax elements of "complex" arguments.
+ * @stable ICU 4.8
+ */
+ public static final class Part {
+ private Part(Type t, int i, int l, int v) {
+ type=t;
+ index=i;
+ length=(char)l;
+ value=(short)v;
+ }
+
+ /**
+ * Returns the type of this part.
+ * @return the part type.
+ * @stable ICU 4.8
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Returns the pattern string index associated with this Part.
+ * @return this part's pattern string index.
+ * @stable ICU 4.8
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Returns the length of the pattern substring associated with this Part.
+ * This is 0 for some parts.
+ * @return this part's pattern substring length.
+ * @stable ICU 4.8
+ */
+ public int getLength() {
+ return length;
+ }
+
+ /**
+ * Returns the pattern string limit (exclusive-end) index associated with this Part.
+ * Convenience method for getIndex()+getLength().
+ * @return this part's pattern string limit index, same as getIndex()+getLength().
+ * @stable ICU 4.8
+ */
+ public int getLimit() {
+ return index+length;
+ }
+
+ /**
+ * Returns a value associated with this part.
+ * See the documentation of each part type for details.
+ * @return the part value.
+ * @stable ICU 4.8
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Returns the argument type if this part is of type ARG_START or ARG_LIMIT,
+ * otherwise ArgType.NONE.
+ * @return the argument type for this part.
+ * @stable ICU 4.8
+ */
+ public ArgType getArgType() {
+ Type type=getType();
+ if(type==Type.ARG_START || type==Type.ARG_LIMIT) {
+ return argTypes[value];
+ } else {
+ return ArgType.NONE;
+ }
+ }
+
+ /**
+ * Part type constants.
+ * @stable ICU 4.8
+ */
+ public enum Type {
+ /**
+ * Start of a message pattern (main or nested).
+ * The length is 0 for the top-level message
+ * and for a choice argument sub-message, otherwise 1 for the '{'.
+ * The value indicates the nesting level, starting with 0 for the main message.
+ * <p>
+ * There is always a later MSG_LIMIT part.
+ * @stable ICU 4.8
+ */
+ MSG_START,
+ /**
+ * End of a message pattern (main or nested).
+ * The length is 0 for the top-level message and
+ * the last sub-message of a choice argument,
+ * otherwise 1 for the '}' or (in a choice argument style) the '|'.
+ * The value indicates the nesting level, starting with 0 for the main message.
+ * @stable ICU 4.8
+ */
+ MSG_LIMIT,
+ /**
+ * Indicates a substring of the pattern string which is to be skipped when formatting.
+ * For example, an apostrophe that begins or ends quoted text
+ * would be indicated with such a part.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ SKIP_SYNTAX,
+ /**
+ * Indicates that a syntax character needs to be inserted for auto-quoting.
+ * The length is 0.
+ * The value is the character code of the insertion character. (U+0027=APOSTROPHE)
+ * @stable ICU 4.8
+ */
+ INSERT_CHAR,
+ /**
+ * Indicates a syntactic (non-escaped) # symbol in a plural variant.
+ * When formatting, replace this part's substring with the
+ * (value-offset) for the plural argument value.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ REPLACE_NUMBER,
+ /**
+ * Start of an argument.
+ * The length is 1 for the '{'.
+ * The value is the ordinal value of the ArgType. Use getArgType().
+ * <p>
+ * This part is followed by either an ARG_NUMBER or ARG_NAME,
+ * followed by optional argument sub-parts (see ArgType constants)
+ * and finally an ARG_LIMIT part.
+ * @stable ICU 4.8
+ */
+ ARG_START,
+ /**
+ * End of an argument.
+ * The length is 1 for the '}'.
+ * The value is the ordinal value of the ArgType. Use getArgType().
+ * @stable ICU 4.8
+ */
+ ARG_LIMIT,
+ /**
+ * The argument number, provided by the value.
+ * @stable ICU 4.8
+ */
+ ARG_NUMBER,
+ /**
+ * The argument name.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ ARG_NAME,
+ /**
+ * The argument type.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ ARG_TYPE,
+ /**
+ * The argument style text.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ ARG_STYLE,
+ /**
+ * A selector substring in a "complex" argument style.
+ * The value is undefined and currently always 0.
+ * @stable ICU 4.8
+ */
+ ARG_SELECTOR,
+ /**
+ * An integer value, for example the offset or an explicit selector value
+ * in a PluralFormat style.
+ * The part value is the integer value.
+ * @stable ICU 4.8
+ */
+ ARG_INT,
+ /**
+ * A numeric value, for example the offset or an explicit selector value
+ * in a PluralFormat style.
+ * The part value is an index into an internal array of numeric values;
+ * use getNumericValue().
+ * @stable ICU 4.8
+ */
+ ARG_DOUBLE;
+
+ /**
+ * Indicates whether this part has a numeric value.
+ * If so, then that numeric value can be retrieved via {@link MessagePattern#getNumericValue(Part)}.
+ * @return true if this part has a numeric value.
+ * @stable ICU 4.8
+ */
+ public boolean hasNumericValue() {
+ return this==ARG_INT || this==ARG_DOUBLE;
+ }
+ }
+
+ /**
+ * @return a string representation of this part.
+ * @stable ICU 4.8
+ */
+ @Override
+ public String toString() {
+ String valueString=(type==Type.ARG_START || type==Type.ARG_LIMIT) ?
+ getArgType().name() : Integer.toString(value);
+ return type.name()+"("+valueString+")@"+index;
+ }
+
+ /**
+ * @param other another object to compare with.
+ * @return true if this object is equivalent to the other one.
+ * @stable ICU 4.8
+ */
+ @Override
+ public boolean equals(Object other) {
+ if(this==other) {
+ return true;
+ }
+ if(other==null || getClass()!=other.getClass()) {
+ return false;
+ }
+ Part o=(Part)other;
+ return
+ type.equals(o.type) &&
+ index==o.index &&
+ length==o.length &&
+ value==o.value &&
+ limitPartIndex==o.limitPartIndex;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 4.8
+ */
+ @Override
+ public int hashCode() {
+ return ((type.hashCode()*37+index)*37+length)*37+value;
+ }
+
+ private static final int MAX_LENGTH=0xffff;
+ private static final int MAX_VALUE=Short.MAX_VALUE;
+
+ // Some fields are not final because they are modified during pattern parsing.
+ // After pattern parsing, the parts are effectively immutable.
+ private final Type type;
+ private final int index;
+ private final char length;
+ private short value;
+ private int limitPartIndex;
+ }
+
+ /**
+ * Argument type constants.
+ * Returned by Part.getArgType() for ARG_START and ARG_LIMIT parts.
+ *
+ * Messages nested inside an argument are each delimited by MSG_START and MSG_LIMIT,
+ * with a nesting level one greater than the surrounding message.
+ * @stable ICU 4.8
+ */
+ public enum ArgType {
+ /**
+ * The argument has no specified type.
+ * @stable ICU 4.8
+ */
+ NONE,
+ /**
+ * The argument has a "simple" type which is provided by the ARG_TYPE part.
+ * An ARG_STYLE part might follow that.
+ * @stable ICU 4.8
+ */
+ SIMPLE,
+ /**
+ * The argument is a ChoiceFormat with one or more
+ * ((ARG_INT | ARG_DOUBLE), ARG_SELECTOR, message) tuples.
+ * @stable ICU 4.8
+ */
+ CHOICE,
+ /**
+ * The argument is a cardinal-number PluralFormat with an optional ARG_INT or ARG_DOUBLE offset
+ * (e.g., offset:1)
+ * and one or more (ARG_SELECTOR [explicit-value] message) tuples.
+ * If the selector has an explicit value (e.g., =2), then
+ * that value is provided by the ARG_INT or ARG_DOUBLE part preceding the message.
+ * Otherwise the message immediately follows the ARG_SELECTOR.
+ * @stable ICU 4.8
+ */
+ PLURAL,
+ /**
+ * The argument is a SelectFormat with one or more (ARG_SELECTOR, message) pairs.
+ * @stable ICU 4.8
+ */
+ SELECT,
+ /**
+ * The argument is an ordinal-number PluralFormat
+ * with the same style parts sequence and semantics as {@link ArgType#PLURAL}.
+ * @stable ICU 50
+ */
+ SELECTORDINAL;
+
+ /**
+ * @return true if the argument type has a plural style part sequence and semantics,
+ * for example {@link ArgType#PLURAL} and {@link ArgType#SELECTORDINAL}.
+ * @stable ICU 50
+ */
+ public boolean hasPluralStyle() {
+ return this == PLURAL || this == SELECTORDINAL;
+ }
+ }
+
+ /**
+ * Creates and returns a copy of this object.
+ * @return a copy of this object (or itself if frozen).
+ * @stable ICU 4.8
+ */
+ @Override
+ public Object clone() {
+ if(isFrozen()) {
+ return this;
+ } else {
+ return cloneAsThawed();
+ }
+ }
+
+ /**
+ * Creates and returns an unfrozen copy of this object.
+ * @return a copy of this object.
+ * @stable ICU 4.8
+ */
+ @SuppressWarnings("unchecked")
+ public MessagePattern cloneAsThawed() {
+ MessagePattern newMsg;
+ try {
+ newMsg=(MessagePattern)super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ newMsg.parts=(ArrayList<Part>)parts.clone();
+ if(numericValues!=null) {
+ newMsg.numericValues=(ArrayList<Double>)numericValues.clone();
+ }
+ newMsg.frozen=false;
+ return newMsg;
+ }
+
+ /**
+ * Freezes this object, making it immutable and thread-safe.
+ * @return this
+ * @stable ICU 4.8
+ */
+ public MessagePattern freeze() {
+ frozen=true;
+ return this;
+ }
+
+ /**
+ * Determines whether this object is frozen (immutable) or not.
+ * @return true if this object is frozen.
+ * @stable ICU 4.8
+ */
+ public boolean isFrozen() {
+ return frozen;
+ }
+
+ private void preParse(String pattern) {
+ if(isFrozen()) {
+ throw new UnsupportedOperationException(
+ "Attempt to parse("+prefix(pattern)+") on frozen MessagePattern instance.");
+ }
+ msg=pattern;
+ hasArgNames=hasArgNumbers=false;
+ needsAutoQuoting=false;
+ parts.clear();
+ if(numericValues!=null) {
+ numericValues.clear();
+ }
+ }
+
+ private void postParse() {
+ // Nothing to be done currently.
+ }
+
+ private int parseMessage(int index, int msgStartLength, int nestingLevel, ArgType parentType) {
+ if(nestingLevel>Part.MAX_VALUE) {
+ throw new IndexOutOfBoundsException();
+ }
+ int msgStart=parts.size();
+ addPart(Part.Type.MSG_START, index, msgStartLength, nestingLevel);
+ index+=msgStartLength;
+ while(index<msg.length()) {
+ char c=msg.charAt(index++);
+ if(c=='\'') {
+ if(index==msg.length()) {
+ // The apostrophe is the last character in the pattern.
+ // Add a Part for auto-quoting.
+ addPart(Part.Type.INSERT_CHAR, index, 0, '\''); // value=char to be inserted
+ needsAutoQuoting=true;
+ } else {
+ c=msg.charAt(index);
+ if(c=='\'') {
+ // double apostrophe, skip the second one
+ addPart(Part.Type.SKIP_SYNTAX, index++, 1, 0);
+ } else if(
+ aposMode==ApostropheMode.DOUBLE_REQUIRED ||
+ c=='{' || c=='}' ||
+ (parentType==ArgType.CHOICE && c=='|') ||
+ (parentType.hasPluralStyle() && c=='#')
+ ) {
+ // skip the quote-starting apostrophe
+ addPart(Part.Type.SKIP_SYNTAX, index-1, 1, 0);
+ // find the end of the quoted literal text
+ for(;;) {
+ index=msg.indexOf('\'', index+1);
+ if(index>=0) {
+ if((index+1)<msg.length() && msg.charAt(index+1)=='\'') {
+ // double apostrophe inside quoted literal text
+ // still encodes a single apostrophe, skip the second one
+ addPart(Part.Type.SKIP_SYNTAX, ++index, 1, 0);
+ } else {
+ // skip the quote-ending apostrophe
+ addPart(Part.Type.SKIP_SYNTAX, index++, 1, 0);
+ break;
+ }
+ } else {
+ // The quoted text reaches to the end of the of the message.
+ index=msg.length();
+ // Add a Part for auto-quoting.
+ addPart(Part.Type.INSERT_CHAR, index, 0, '\''); // value=char to be inserted
+ needsAutoQuoting=true;
+ break;
+ }
+ }
+ } else {
+ // Interpret the apostrophe as literal text.
+ // Add a Part for auto-quoting.
+ addPart(Part.Type.INSERT_CHAR, index, 0, '\''); // value=char to be inserted
+ needsAutoQuoting=true;
+ }
+ }
+ } else if(parentType.hasPluralStyle() && c=='#') {
+ // The unquoted # in a plural message fragment will be replaced
+ // with the (number-offset).
+ addPart(Part.Type.REPLACE_NUMBER, index-1, 1, 0);
+ } else if(c=='{') {
+ index=parseArg(index-1, 1, nestingLevel);
+ } else if((nestingLevel>0 && c=='}') || (parentType==ArgType.CHOICE && c=='|')) {
+ // Finish the message before the terminator.
+ // In a choice style, report the "}" substring only for the following ARG_LIMIT,
+ // not for this MSG_LIMIT.
+ int limitLength=(parentType==ArgType.CHOICE && c=='}') ? 0 : 1;
+ addLimitPart(msgStart, Part.Type.MSG_LIMIT, index-1, limitLength, nestingLevel);
+ if(parentType==ArgType.CHOICE) {
+ // Let the choice style parser see the '}' or '|'.
+ return index-1;
+ } else {
+ // continue parsing after the '}'
+ return index;
+ }
+ } // else: c is part of literal text
+ }
+ if(nestingLevel>0 && !inTopLevelChoiceMessage(nestingLevel, parentType)) {
+ throw new IllegalArgumentException(
+ "Unmatched '{' braces in message "+prefix());
+ }
+ addLimitPart(msgStart, Part.Type.MSG_LIMIT, index, 0, nestingLevel);
+ return index;
+ }
+
+ private int parseArg(int index, int argStartLength, int nestingLevel) {
+ int argStart=parts.size();
+ ArgType argType=ArgType.NONE;
+ addPart(Part.Type.ARG_START, index, argStartLength, argType.ordinal());
+ int nameIndex=index=skipWhiteSpace(index+argStartLength);
+ if(index==msg.length()) {
+ throw new IllegalArgumentException(
+ "Unmatched '{' braces in message "+prefix());
+ }
+ // parse argument name or number
+ index=skipIdentifier(index);
+ int number=parseArgNumber(nameIndex, index);
+ if(number>=0) {
+ int length=index-nameIndex;
+ if(length>Part.MAX_LENGTH || number>Part.MAX_VALUE) {
+ throw new IndexOutOfBoundsException(
+ "Argument number too large: "+prefix(nameIndex));
+ }
+ hasArgNumbers=true;
+ addPart(Part.Type.ARG_NUMBER, nameIndex, length, number);
+ } else if(number==ARG_NAME_NOT_NUMBER) {
+ int length=index-nameIndex;
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Argument name too long: "+prefix(nameIndex));
+ }
+ hasArgNames=true;
+ addPart(Part.Type.ARG_NAME, nameIndex, length, 0);
+ } else { // number<-1 (ARG_NAME_NOT_VALID)
+ throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
+ }
+ index=skipWhiteSpace(index);
+ if(index==msg.length()) {
+ throw new IllegalArgumentException(
+ "Unmatched '{' braces in message "+prefix());
+ }
+ char c=msg.charAt(index);
+ if(c=='}') {
+ // all done
+ } else if(c!=',') {
+ throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
+ } else /* ',' */ {
+ // parse argument type: case-sensitive a-zA-Z
+ int typeIndex=index=skipWhiteSpace(index+1);
+ while(index<msg.length() && isArgTypeChar(msg.charAt(index))) {
+ ++index;
+ }
+ int length=index-typeIndex;
+ index=skipWhiteSpace(index);
+ if(index==msg.length()) {
+ throw new IllegalArgumentException(
+ "Unmatched '{' braces in message "+prefix());
+ }
+ if(length==0 || ((c=msg.charAt(index))!=',' && c!='}')) {
+ throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
+ }
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Argument type name too long: "+prefix(nameIndex));
+ }
+ argType=ArgType.SIMPLE;
+ if(length==6) {
+ // case-insensitive comparisons for complex-type names
+ if(isChoice(typeIndex)) {
+ argType=ArgType.CHOICE;
+ } else if(isPlural(typeIndex)) {
+ argType=ArgType.PLURAL;
+ } else if(isSelect(typeIndex)) {
+ argType=ArgType.SELECT;
+ }
+ } else if(length==13) {
+ if(isSelect(typeIndex) && isOrdinal(typeIndex+6)) {
+ argType=ArgType.SELECTORDINAL;
+ }
+ }
+ // change the ARG_START type from NONE to argType
+ parts.get(argStart).value=(short)argType.ordinal();
+ if(argType==ArgType.SIMPLE) {
+ addPart(Part.Type.ARG_TYPE, typeIndex, length, 0);
+ }
+ // look for an argument style (pattern)
+ if(c=='}') {
+ if(argType!=ArgType.SIMPLE) {
+ throw new IllegalArgumentException(
+ "No style field for complex argument: "+prefix(nameIndex));
+ }
+ } else /* ',' */ {
+ ++index;
+ if(argType==ArgType.SIMPLE) {
+ index=parseSimpleStyle(index);
+ } else if(argType==ArgType.CHOICE) {
+ index=parseChoiceStyle(index, nestingLevel);
+ } else {
+ index=parsePluralOrSelectStyle(argType, index, nestingLevel);
+ }
+ }
+ }
+ // Argument parsing stopped on the '}'.
+ addLimitPart(argStart, Part.Type.ARG_LIMIT, index, 1, argType.ordinal());
+ return index+1;
+ }
+
+ private int parseSimpleStyle(int index) {
+ int start=index;
+ int nestedBraces=0;
+ while(index<msg.length()) {
+ char c=msg.charAt(index++);
+ if(c=='\'') {
+ // Treat apostrophe as quoting but include it in the style part.
+ // Find the end of the quoted literal text.
+ index=msg.indexOf('\'', index);
+ if(index<0) {
+ throw new IllegalArgumentException(
+ "Quoted literal argument style text reaches to the end of the message: "+
+ prefix(start));
+ }
+ // skip the quote-ending apostrophe
+ ++index;
+ } else if(c=='{') {
+ ++nestedBraces;
+ } else if(c=='}') {
+ if(nestedBraces>0) {
+ --nestedBraces;
+ } else {
+ int length=--index-start;
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Argument style text too long: "+prefix(start));
+ }
+ addPart(Part.Type.ARG_STYLE, start, length, 0);
+ return index;
+ }
+ } // c is part of literal text
+ }
+ throw new IllegalArgumentException(
+ "Unmatched '{' braces in message "+prefix());
+ }
+
+ private int parseChoiceStyle(int index, int nestingLevel) {
+ int start=index;
+ index=skipWhiteSpace(index);
+ if(index==msg.length() || msg.charAt(index)=='}') {
+ throw new IllegalArgumentException(
+ "Missing choice argument pattern in "+prefix());
+ }
+ for(;;) {
+ // The choice argument style contains |-separated (number, separator, message) triples.
+ // Parse the number.
+ int numberIndex=index;
+ index=skipDouble(index);
+ int length=index-numberIndex;
+ if(length==0) {
+ throw new IllegalArgumentException("Bad choice pattern syntax: "+prefix(start));
+ }
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Choice number too long: "+prefix(numberIndex));
+ }
+ parseDouble(numberIndex, index, true); // adds ARG_INT or ARG_DOUBLE
+ // Parse the separator.
+ index=skipWhiteSpace(index);
+ if(index==msg.length()) {
+ throw new IllegalArgumentException("Bad choice pattern syntax: "+prefix(start));
+ }
+ char c=msg.charAt(index);
+ if(!(c=='#' || c=='<' || c=='\u2264')) { // U+2264 is <=
+ throw new IllegalArgumentException(
+ "Expected choice separator (#<\u2264) instead of '"+c+
+ "' in choice pattern "+prefix(start));
+ }
+ addPart(Part.Type.ARG_SELECTOR, index, 1, 0);
+ // Parse the message fragment.
+ index=parseMessage(++index, 0, nestingLevel+1, ArgType.CHOICE);
+ // parseMessage(..., CHOICE) returns the index of the terminator, or msg.length().
+ if(index==msg.length()) {
+ return index;
+ }
+ if(msg.charAt(index)=='}') {
+ if(!inMessageFormatPattern(nestingLevel)) {
+ throw new IllegalArgumentException(
+ "Bad choice pattern syntax: "+prefix(start));
+ }
+ return index;
+ } // else the terminator is '|'
+ index=skipWhiteSpace(index+1);
+ }
+ }
+
+ private int parsePluralOrSelectStyle(ArgType argType, int index, int nestingLevel) {
+ int start=index;
+ boolean isEmpty=true;
+ boolean hasOther=false;
+ for(;;) {
+ // First, collect the selector looking for a small set of terminators.
+ // It would be a little faster to consider the syntax of each possible
+ // token right here, but that makes the code too complicated.
+ index=skipWhiteSpace(index);
+ boolean eos=index==msg.length();
+ if(eos || msg.charAt(index)=='}') {
+ if(eos==inMessageFormatPattern(nestingLevel)) {
+ throw new IllegalArgumentException(
+ "Bad "+
+ argType.toString().toLowerCase(Locale.ENGLISH)+
+ " pattern syntax: "+prefix(start));
+ }
+ if(!hasOther) {
+ throw new IllegalArgumentException(
+ "Missing 'other' keyword in "+
+ argType.toString().toLowerCase(Locale.ENGLISH)+
+ " pattern in "+prefix());
+ }
+ return index;
+ }
+ int selectorIndex=index;
+ if(argType.hasPluralStyle() && msg.charAt(selectorIndex)=='=') {
+ // explicit-value plural selector: =double
+ index=skipDouble(index+1);
+ int length=index-selectorIndex;
+ if(length==1) {
+ throw new IllegalArgumentException(
+ "Bad "+
+ argType.toString().toLowerCase(Locale.ENGLISH)+
+ " pattern syntax: "+prefix(start));
+ }
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Argument selector too long: "+prefix(selectorIndex));
+ }
+ addPart(Part.Type.ARG_SELECTOR, selectorIndex, length, 0);
+ parseDouble(selectorIndex+1, index, false); // adds ARG_INT or ARG_DOUBLE
+ } else {
+ index=skipIdentifier(index);
+ int length=index-selectorIndex;
+ if(length==0) {
+ throw new IllegalArgumentException(
+ "Bad "+
+ argType.toString().toLowerCase(Locale.ENGLISH)+
+ " pattern syntax: "+prefix(start));
+ }
+ // Note: The ':' in "offset:" is just beyond the skipIdentifier() range.
+ if( argType.hasPluralStyle() && length==6 && index<msg.length() &&
+ msg.regionMatches(selectorIndex, "offset:", 0, 7)
+ ) {
+ // plural offset, not a selector
+ if(!isEmpty) {
+ throw new IllegalArgumentException(
+ "Plural argument 'offset:' (if present) must precede key-message pairs: "+
+ prefix(start));
+ }
+ // allow whitespace between offset: and its value
+ int valueIndex=skipWhiteSpace(index+1); // The ':' is at index.
+ index=skipDouble(valueIndex);
+ if(index==valueIndex) {
+ throw new IllegalArgumentException(
+ "Missing value for plural 'offset:' "+prefix(start));
+ }
+ if((index-valueIndex)>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Plural offset value too long: "+prefix(valueIndex));
+ }
+ parseDouble(valueIndex, index, false); // adds ARG_INT or ARG_DOUBLE
+ isEmpty=false;
+ continue; // no message fragment after the offset
+ } else {
+ // normal selector word
+ if(length>Part.MAX_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "Argument selector too long: "+prefix(selectorIndex));
+ }
+ addPart(Part.Type.ARG_SELECTOR, selectorIndex, length, 0);
+ if(msg.regionMatches(selectorIndex, "other", 0, length)) {
+ hasOther=true;
+ }
+ }
+ }
+
+ // parse the message fragment following the selector
+ index=skipWhiteSpace(index);
+ if(index==msg.length() || msg.charAt(index)!='{') {
+ throw new IllegalArgumentException(
+ "No message fragment after "+
+ argType.toString().toLowerCase(Locale.ENGLISH)+
+ " selector: "+prefix(selectorIndex));
+ }
+ index=parseMessage(index, 1, nestingLevel+1, argType);
+ isEmpty=false;
+ }
+ }
+
+ /**
+ * Validates and parses an argument name or argument number string.
+ * This internal method assumes that the input substring is a "pattern identifier".
+ * @return &gt;=0 if the name is a valid number,
+ * ARG_NAME_NOT_NUMBER (-1) if it is a "pattern identifier" but not all ASCII digits,
+ * ARG_NAME_NOT_VALID (-2) if it is neither.
+ * @see #validateArgumentName(String)
+ */
+ private static int parseArgNumber(CharSequence s, int start, int limit) {
+ // If the identifier contains only ASCII digits, then it is an argument _number_
+ // and must not have leading zeros (except "0" itself).
+ // Otherwise it is an argument _name_.
+ if(start>=limit) {
+ return ARG_NAME_NOT_VALID;
+ }
+ int number;
+ // Defer numeric errors until we know there are only digits.
+ boolean badNumber;
+ char c=s.charAt(start++);
+ if(c=='0') {
+ if(start==limit) {
+ return 0;
+ } else {
+ number=0;
+ badNumber=true; // leading zero
+ }
+ } else if('1'<=c && c<='9') {
+ number=c-'0';
+ badNumber=false;
+ } else {
+ return ARG_NAME_NOT_NUMBER;
+ }
+ while(start<limit) {
+ c=s.charAt(start++);
+ if('0'<=c && c<='9') {
+ if(number>=Integer.MAX_VALUE/10) {
+ badNumber=true; // overflow
+ }
+ number=number*10+(c-'0');
+ } else {
+ return ARG_NAME_NOT_NUMBER;
+ }
+ }
+ // There are only ASCII digits.
+ if(badNumber) {
+ return ARG_NAME_NOT_VALID;
+ } else {
+ return number;
+ }
+ }
+
+ private int parseArgNumber(int start, int limit) {
+ return parseArgNumber(msg, start, limit);
+ }
+
+ /**
+ * Parses a number from the specified message substring.
+ * @param start start index into the message string
+ * @param limit limit index into the message string, must be start<limit
+ * @param allowInfinity true if U+221E is allowed (for ChoiceFormat)
+ */
+ private void parseDouble(int start, int limit, boolean allowInfinity) {
+ assert start<limit;
+ // fake loop for easy exit and single throw statement
+ for(;;) {
+ // fast path for small integers and infinity
+ int value=0;
+ int isNegative=0; // not boolean so that we can easily add it to value
+ int index=start;
+ char c=msg.charAt(index++);
+ if(c=='-') {
+ isNegative=1;
+ if(index==limit) {
+ break; // no number
+ }
+ c=msg.charAt(index++);
+ } else if(c=='+') {
+ if(index==limit) {
+ break; // no number
+ }
+ c=msg.charAt(index++);
+ }
+ if(c==0x221e) { // infinity
+ if(allowInfinity && index==limit) {
+ addArgDoublePart(
+ isNegative!=0 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY,
+ start, limit-start);
+ return;
+ } else {
+ break;
+ }
+ }
+ // try to parse the number as a small integer but fall back to a double
+ while('0'<=c && c<='9') {
+ value=value*10+(c-'0');
+ if(value>(Part.MAX_VALUE+isNegative)) {
+ break; // not a small-enough integer
+ }
+ if(index==limit) {
+ addPart(Part.Type.ARG_INT, start, limit-start, isNegative!=0 ? -value : value);
+ return;
+ }
+ c=msg.charAt(index++);
+ }
+ // Let Double.parseDouble() throw a NumberFormatException.
+ double numericValue=Double.parseDouble(msg.substring(start, limit));
+ addArgDoublePart(numericValue, start, limit-start);
+ return;
+ }
+ throw new NumberFormatException(
+ "Bad syntax for numeric value: "+msg.substring(start, limit));
+ }
+
+ /**
+ * Appends the s[start, limit[ substring to sb, but with only half of the apostrophes
+ * according to JDK pattern behavior.
+ * @internal
+ */
+ /* package */ static void appendReducedApostrophes(String s, int start, int limit,
+ StringBuilder sb) {
+ int doubleApos=-1;
+ for(;;) {
+ int i=s.indexOf('\'', start);
+ if(i<0 || i>=limit) {
+ sb.append(s, start, limit);
+ break;
+ }
+ if(i==doubleApos) {
+ // Double apostrophe at start-1 and start==i, append one.
+ sb.append('\'');
+ ++start;
+ doubleApos=-1;
+ } else {
+ // Append text between apostrophes and skip this one.
+ sb.append(s, start, i);
+ doubleApos=start=i+1;
+ }
+ }
+ }
+
+ private int skipWhiteSpace(int index) {
+ return PatternProps.skipWhiteSpace(msg, index);
+ }
+
+ private int skipIdentifier(int index) {
+ return PatternProps.skipIdentifier(msg, index);
+ }
+
+ /**
+ * Skips a sequence of characters that could occur in a double value.
+ * Does not fully parse or validate the value.
+ */
+ private int skipDouble(int index) {
+ while(index<msg.length()) {
+ char c=msg.charAt(index);
+ // U+221E: Allow the infinity symbol, for ChoiceFormat patterns.
+ if((c<'0' && "+-.".indexOf(c)<0) || (c>'9' && c!='e' && c!='E' && c!=0x221e)) {
+ break;
+ }
+ ++index;
+ }
+ return index;
+ }
+
+ private static boolean isArgTypeChar(int c) {
+ return ('a'<=c && c<='z') || ('A'<=c && c<='Z');
+ }
+
+ private boolean isChoice(int index) {
+ char c;
+ return
+ ((c=msg.charAt(index++))=='c' || c=='C') &&
+ ((c=msg.charAt(index++))=='h' || c=='H') &&
+ ((c=msg.charAt(index++))=='o' || c=='O') &&
+ ((c=msg.charAt(index++))=='i' || c=='I') &&
+ ((c=msg.charAt(index++))=='c' || c=='C') &&
+ ((c=msg.charAt(index))=='e' || c=='E');
+ }
+
+ private boolean isPlural(int index) {
+ char c;
+ return
+ ((c=msg.charAt(index++))=='p' || c=='P') &&
+ ((c=msg.charAt(index++))=='l' || c=='L') &&
+ ((c=msg.charAt(index++))=='u' || c=='U') &&
+ ((c=msg.charAt(index++))=='r' || c=='R') &&
+ ((c=msg.charAt(index++))=='a' || c=='A') &&
+ ((c=msg.charAt(index))=='l' || c=='L');
+ }
+
+ private boolean isSelect(int index) {
+ char c;
+ return
+ ((c=msg.charAt(index++))=='s' || c=='S') &&
+ ((c=msg.charAt(index++))=='e' || c=='E') &&
+ ((c=msg.charAt(index++))=='l' || c=='L') &&
+ ((c=msg.charAt(index++))=='e' || c=='E') &&
+ ((c=msg.charAt(index++))=='c' || c=='C') &&
+ ((c=msg.charAt(index))=='t' || c=='T');
+ }
+
+ private boolean isOrdinal(int index) {
+ char c;
+ return
+ ((c=msg.charAt(index++))=='o' || c=='O') &&
+ ((c=msg.charAt(index++))=='r' || c=='R') &&
+ ((c=msg.charAt(index++))=='d' || c=='D') &&
+ ((c=msg.charAt(index++))=='i' || c=='I') &&
+ ((c=msg.charAt(index++))=='n' || c=='N') &&
+ ((c=msg.charAt(index++))=='a' || c=='A') &&
+ ((c=msg.charAt(index))=='l' || c=='L');
+ }
+
+ /**
+ * @return true if we are inside a MessageFormat (sub-)pattern,
+ * as opposed to inside a top-level choice/plural/select pattern.
+ */
+ private boolean inMessageFormatPattern(int nestingLevel) {
+ return nestingLevel>0 || parts.get(0).type==Part.Type.MSG_START;
+ }
+
+ /**
+ * @return true if we are in a MessageFormat sub-pattern
+ * of a top-level ChoiceFormat pattern.
+ */
+ private boolean inTopLevelChoiceMessage(int nestingLevel, ArgType parentType) {
+ return
+ nestingLevel==1 &&
+ parentType==ArgType.CHOICE &&
+ parts.get(0).type!=Part.Type.MSG_START;
+ }
+
+ private void addPart(Part.Type type, int index, int length, int value) {
+ parts.add(new Part(type, index, length, value));
+ }
+
+ private void addLimitPart(int start, Part.Type type, int index, int length, int value) {
+ parts.get(start).limitPartIndex=parts.size();
+ addPart(type, index, length, value);
+ }
+
+ private void addArgDoublePart(double numericValue, int start, int length) {
+ int numericIndex;
+ if(numericValues==null) {
+ numericValues=new ArrayList<Double>();
+ numericIndex=0;
+ } else {
+ numericIndex=numericValues.size();
+ if(numericIndex>Part.MAX_VALUE) {
+ throw new IndexOutOfBoundsException("Too many numeric values");
+ }
+ }
+ numericValues.add(numericValue);
+ addPart(Part.Type.ARG_DOUBLE, start, length, numericIndex);
+ }
+
+ private static final int MAX_PREFIX_LENGTH=24;
+
+ /**
+ * Returns a prefix of s.substring(start). Used for Exception messages.
+ * @param s
+ * @param start start index in s
+ * @return s.substring(start) or a prefix of that
+ */
+ private static String prefix(String s, int start) {
+ StringBuilder prefix=new StringBuilder(MAX_PREFIX_LENGTH+20);
+ if(start==0) {
+ prefix.append("\"");
+ } else {
+ prefix.append("[at pattern index ").append(start).append("] \"");
+ }
+ int substringLength=s.length()-start;
+ if(substringLength<=MAX_PREFIX_LENGTH) {
+ prefix.append(start==0 ? s : s.substring(start));
+ } else {
+ int limit=start+MAX_PREFIX_LENGTH-4;
+ if(Character.isHighSurrogate(s.charAt(limit-1))) {
+ // remove lead surrogate from the end of the prefix
+ --limit;
+ }
+ prefix.append(s, start, limit).append(" ...");
+ }
+ return prefix.append("\"").toString();
+ }
+
+ private static String prefix(String s) {
+ return prefix(s, 0);
+ }
+
+ private String prefix(int start) {
+ return prefix(msg, start);
+ }
+
+ private String prefix() {
+ return prefix(msg, 0);
+ }
+
+ private ApostropheMode aposMode;
+ private String msg;
+ private ArrayList<Part> parts=new ArrayList<Part>();
+ private ArrayList<Double> numericValues;
+ private boolean hasArgNames;
+ private boolean hasArgNumbers;
+ private boolean needsAutoQuoting;
+ private boolean frozen;
+
+ private static final ApostropheMode defaultAposMode=
+ ApostropheMode.valueOf(
+ ICUConfig.get("com.ibm.icu.text.MessagePattern.ApostropheMode", "DOUBLE_OPTIONAL"));
+
+ private static final ArgType[] argTypes=ArgType.values();
+}
diff --git a/src/main/com/android/i18n/PatternProps.java b/src/main/com/android/i18n/PatternProps.java
new file mode 100644
index 0000000..7da0f4c
--- /dev/null
+++ b/src/main/com/android/i18n/PatternProps.java
@@ -0,0 +1,264 @@
+/*
+*******************************************************************************
+* Copyright (C) 2011, International Business Machines
+* Corporation and others. All Rights Reserved.
+*******************************************************************************
+* created on: 2011feb25
+* created by: Markus W. Scherer
+*/
+
+package com.ibm.icu.impl;
+
+/**
+ * Implements the immutable Unicode properties Pattern_Syntax and Pattern_White_Space.
+ * Hardcodes these properties, does not load data, does not depend on other ICU classes.
+ * <p>
+ * Note: Both properties include ASCII as well as non-ASCII, non-Latin-1 code points,
+ * and both properties only include BMP code points (no supplementary ones).
+ * Pattern_Syntax includes some unassigned code points.
+ * <p>
+ * [:Pattern_White_Space:] =
+ * [\u0009-\u000D\ \u0085\u200E\u200F\u2028\u2029]
+ * <p>
+ * [:Pattern_Syntax:] =
+ * [!-/\:-@\[-\^`\{-~\u00A1-\u00A7\u00A9\u00AB\u00AC\u00AE
+ * \u00B0\u00B1\u00B6\u00BB\u00BF\u00D7\u00F7
+ * \u2010-\u2027\u2030-\u203E\u2041-\u2053\u2055-\u205E
+ * \u2190-\u245F\u2500-\u2775\u2794-\u2BFF\u2E00-\u2E7F
+ * \u3001-\u3003\u3008-\u3020\u3030\uFD3E\uFD3F\uFE45\uFE46]
+ * @author mscherer
+ */
+public final class PatternProps {
+ /**
+ * @return true if c is a Pattern_Syntax code point.
+ */
+ public static boolean isSyntax(int c) {
+ if(c<0) {
+ return false;
+ } else if(c<=0xff) {
+ return latin1[c]==3;
+ } else if(c<0x2010) {
+ return false;
+ } else if(c<=0x3030) {
+ int bits=syntax2000[index2000[(c-0x2000)>>5]];
+ return ((bits>>(c&0x1f))&1)!=0;
+ } else if(0xfd3e<=c && c<=0xfe46) {
+ return c<=0xfd3f || 0xfe45<=c;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return true if c is a Pattern_Syntax or Pattern_White_Space code point.
+ */
+ public static boolean isSyntaxOrWhiteSpace(int c) {
+ if(c<0) {
+ return false;
+ } else if(c<=0xff) {
+ return latin1[c]!=0;
+ } else if(c<0x200e) {
+ return false;
+ } else if(c<=0x3030) {
+ int bits=syntaxOrWhiteSpace2000[index2000[(c-0x2000)>>5]];
+ return ((bits>>(c&0x1f))&1)!=0;
+ } else if(0xfd3e<=c && c<=0xfe46) {
+ return c<=0xfd3f || 0xfe45<=c;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return true if c is a Pattern_White_Space character.
+ */
+ public static boolean isWhiteSpace(int c) {
+ if(c<0) {
+ return false;
+ } else if(c<=0xff) {
+ return latin1[c]==5;
+ } else if(0x200e<=c && c<=0x2029) {
+ return c<=0x200f || 0x2028<=c;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Skips over Pattern_White_Space starting at index i of the CharSequence.
+ * @return The smallest index at or after i with a non-white space character.
+ */
+ public static int skipWhiteSpace(CharSequence s, int i) {
+ while(i<s.length() && isWhiteSpace(s.charAt(i))) {
+ ++i;
+ }
+ return i;
+ }
+
+ /**
+ * @return s except with leading and trailing Pattern_White_Space removed.
+ */
+ public static String trimWhiteSpace(String s) {
+ if(s.length()==0 || (!isWhiteSpace(s.charAt(0)) && !isWhiteSpace(s.charAt(s.length()-1)))) {
+ return s;
+ }
+ int start=0;
+ int limit=s.length();
+ while(start<limit && isWhiteSpace(s.charAt(start))) {
+ ++start;
+ }
+ if(start<limit) {
+ // There is non-white space at start; we will not move limit below that,
+ // so we need not test start<limit in the loop.
+ while(isWhiteSpace(s.charAt(limit-1))) {
+ --limit;
+ }
+ }
+ return s.substring(start, limit);
+ }
+
+ /**
+ * Tests whether the CharSequence contains a "pattern identifier", that is,
+ * whether it contains only non-Pattern_White_Space, non-Pattern_Syntax characters.
+ * @return true if there are no Pattern_White_Space or Pattern_Syntax characters in s.
+ */
+ public static boolean isIdentifier(CharSequence s) {
+ int limit=s.length();
+ if(limit==0) {
+ return false;
+ }
+ int start=0;
+ do {
+ if(isSyntaxOrWhiteSpace(s.charAt(start++))) {
+ return false;
+ }
+ } while(start<limit);
+ return true;
+ }
+
+ /**
+ * Tests whether the CharSequence contains a "pattern identifier", that is,
+ * whether it contains only non-Pattern_White_Space, non-Pattern_Syntax characters.
+ * @return true if there are no Pattern_White_Space or Pattern_Syntax characters
+ * in s between start and (exclusive) limit.
+ */
+ public static boolean isIdentifier(CharSequence s, int start, int limit) {
+ if(start>=limit) {
+ return false;
+ }
+ do {
+ if(isSyntaxOrWhiteSpace(s.charAt(start++))) {
+ return false;
+ }
+ } while(start<limit);
+ return true;
+ }
+
+ /**
+ * Skips over a "pattern identifier" starting at index i of the CharSequence.
+ * @return The smallest index at or after i with
+ * a Pattern_White_Space or Pattern_Syntax character.
+ */
+ public static int skipIdentifier(CharSequence s, int i) {
+ while(i<s.length() && !isSyntaxOrWhiteSpace(s.charAt(i))) {
+ ++i;
+ }
+ return i;
+ }
+
+ /*
+ * One byte per Latin-1 character.
+ * Bit 0 is set if either Pattern property is true,
+ * bit 1 if Pattern_Syntax is true,
+ * bit 2 if Pattern_White_Space is true.
+ * That is, Pattern_Syntax is encoded as 3 and Pattern_White_Space as 5.
+ */
+ private static final byte latin1[]=new byte[] { // 256
+ // WS: 9..D
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // WS: 20 Syntax: 21..2F
+ 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ // Syntax: 3A..40
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3,
+ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Syntax: 5B..5E
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0,
+ // Syntax: 60
+ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Syntax: 7B..7E
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0,
+ // WS: 85
+ 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Syntax: A1..A7, A9, AB, AC, AE
+ 0, 3, 3, 3, 3, 3, 3, 3, 0, 3, 0, 3, 3, 0, 3, 0,
+ // Syntax: B0, B1, B6, BB, BF
+ 3, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 3,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Syntax: D7
+ 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // Syntax: F7
+ 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+
+ /*
+ * One byte per 32 characters from U+2000..U+303F indexing into
+ * a small table of 32-bit data words.
+ * The first two data words are all-zeros and all-ones.
+ */
+ private static final byte index2000[]=new byte[] { // 130
+ 2, 3, 4, 0, 0, 0, 0, 0, // 20xx
+ 0, 0, 0, 0, 5, 1, 1, 1, // 21xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 22xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 23xx
+ 1, 1, 1, 0, 0, 0, 0, 0, // 24xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 25xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 26xx
+ 1, 1, 1, 6, 7, 1, 1, 1, // 27xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 28xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 29xx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 2Axx
+ 1, 1, 1, 1, 1, 1, 1, 1, // 2Bxx
+ 0, 0, 0, 0, 0, 0, 0, 0, // 2Cxx
+ 0, 0, 0, 0, 0, 0, 0, 0, // 2Dxx
+ 1, 1, 1, 1, 0, 0, 0, 0, // 2Exx
+ 0, 0, 0, 0, 0, 0, 0, 0, // 2Fxx
+ 8, 9 // 3000..303F
+ };
+
+ /*
+ * One 32-bit integer per 32 characters. Ranges of all-false and all-true
+ * are mapped to the first two values, other ranges map to appropriate bit patterns.
+ */
+ private static final int syntax2000[]=new int[] {
+ 0,
+ -1,
+ 0xffff0000, // 2: 2010..201F
+ 0x7fff00ff, // 3: 2020..2027, 2030..203E
+ 0x7feffffe, // 4: 2041..2053, 2055..205E
+ 0xffff0000, // 5: 2190..219F
+ 0x003fffff, // 6: 2760..2775
+ 0xfff00000, // 7: 2794..279F
+ 0xffffff0e, // 8: 3001..3003, 3008..301F
+ 0x00010001 // 9: 3020, 3030
+ };
+
+ /*
+ * Same as syntax2000, but with additional bits set for the
+ * Pattern_White_Space characters 200E 200F 2028 2029.
+ */
+ private static final int syntaxOrWhiteSpace2000[]=new int[] {
+ 0,
+ -1,
+ 0xffffc000, // 2: 200E..201F
+ 0x7fff03ff, // 3: 2020..2029, 2030..203E
+ 0x7feffffe, // 4: 2041..2053, 2055..205E
+ 0xffff0000, // 5: 2190..219F
+ 0x003fffff, // 6: 2760..2775
+ 0xfff00000, // 7: 2794..279F
+ 0xffffff0e, // 8: 3001..3003, 3008..301F
+ 0x00010001 // 9: 3020, 3030
+ };
+}
diff --git a/src/main/com/android/i18n/PluralRules.java b/src/main/com/android/i18n/PluralRules.java
new file mode 100644
index 0000000..10e0be0
--- /dev/null
+++ b/src/main/com/android/i18n/PluralRules.java
@@ -0,0 +1,2212 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2007-2013, International Business Machines Corporation and
+ * others. All Rights Reserved.
+ *******************************************************************************
+ */
+
+package com.ibm.icu.text;
+
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+import com.ibm.icu.impl.PluralRulesLoader;
+import com.ibm.icu.util.Output;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * <p>
+ * Defines rules for mapping non-negative numeric values onto a small set of keywords.
+ * </p>
+ * <p>
+ * Rules are constructed from a text description, consisting of a series of keywords and conditions. The {@link #select}
+ * method examines each condition in order and returns the keyword for the first condition that matches the number. If
+ * none match, {@link #KEYWORD_OTHER} is returned.
+ * </p>
+ * <p>
+ * A PluralRules object is immutable. It contains caches for sample values, but those are synchronized.
+ * <p>
+ * PluralRules is Serializable so that it can be used in formatters, which are serializable.
+ * </p>
+ * <p>
+ * For more information, details, and tips for writing rules, see the <a
+ * href="http://www.unicode.org/draft/reports/tr35/tr35.html#Language_Plural_Rules">LDML spec, C.11 Language Plural
+ * Rules</a>
+ * </p>
+ * <p>
+ * Examples:
+ * </p>
+ *
+ * <pre>
+ * &quot;one: n is 1; few: n in 2..4&quot;
+ * </pre>
+ * <p>
+ * This defines two rules, for 'one' and 'few'. The condition for 'one' is "n is 1" which means that the number must be
+ * equal to 1 for this condition to pass. The condition for 'few' is "n in 2..4" which means that the number must be
+ * between 2 and 4 inclusive - and be an integer - for this condition to pass. All other numbers are assigned the
+ * keyword "other" by the default rule.
+ * </p>
+ *
+ * <pre>
+ * &quot;zero: n is 0; one: n is 1; zero: n mod 100 in 1..19&quot;
+ * </pre>
+ * <p>
+ * This illustrates that the same keyword can be defined multiple times. Each rule is examined in order, and the first
+ * keyword whose condition passes is the one returned. Also notes that a modulus is applied to n in the last rule. Thus
+ * its condition holds for 119, 219, 319...
+ * </p>
+ *
+ * <pre>
+ * &quot;one: n is 1; few: n mod 10 in 2..4 and n mod 100 not in 12..14&quot;
+ * </pre>
+ * <p>
+ * This illustrates conjunction and negation. The condition for 'few' has two parts, both of which must be met:
+ * "n mod 10 in 2..4" and "n mod 100 not in 12..14". The first part applies a modulus to n before the test as in the
+ * previous example. The second part applies a different modulus and also uses negation, thus it matches all numbers
+ * _not_ in 12, 13, 14, 112, 113, 114, 212, 213, 214...
+ * </p>
+ * <p>
+ * Syntax:
+ * </p>
+ * <pre>
+ * rules = rule (';' rule)*
+ * rule = keyword ':' condition
+ * keyword = &lt;identifier&gt;
+ * condition = and_condition ('or' and_condition)*
+ * and_condition = relation ('and' relation)*
+ * relation = not? expr not? rel not? range_list
+ * expr = ('n' | 'i' | 'f' | 'v' | 't') (mod value)?
+ * not = 'not' | '!'
+ * rel = 'in' | 'is' | '=' | '≠' | 'within'
+ * mod = 'mod' | '%'
+ * range_list = (range | value) (',' range_list)*
+ * value = digit+
+ * digit = 0|1|2|3|4|5|6|7|8|9
+ * range = value'..'value
+ * </pre>
+ * <p>Each <b>not</b> term inverts the meaning; however, there should not be more than one of them.</p>
+ * <p>
+ * The i, f, t, and v values are defined as follows:
+ * </p>
+ * <ul>
+ * <li>i to be the integer digits.</li>
+ * <li>f to be the visible decimal digits, as an integer.</li>
+ * <li>t to be the visible decimal digits—without trailing zeros—as an integer.</li>
+ * <li>v to be the number of visible fraction digits.</li>
+ * <li>j is defined to only match integers. That is j is 3 fails if v != 0 (eg for 3.1 or 3.0).</li>
+ * </ul>
+ * <p>
+ * Examples are in the following table:
+ * </p>
+ * <table border='1' style="border-collapse:collapse">
+ * <tbody>
+ * <tr>
+ * <th>n</th>
+ * <th>i</th>
+ * <th>f</th>
+ * <th>v</th>
+ * </tr>
+ * <tr>
+ * <td>1.0</td>
+ * <td>1</td>
+ * <td align="right">0</td>
+ * <td>1</td>
+ * </tr>
+ * <tr>
+ * <td>1.00</td>
+ * <td>1</td>
+ * <td align="right">0</td>
+ * <td>2</td>
+ * </tr>
+ * <tr>
+ * <td>1.3</td>
+ * <td>1</td>
+ * <td align="right">3</td>
+ * <td>1</td>
+ * </tr>
+ * <tr>
+ * <td>1.03</td>
+ * <td>1</td>
+ * <td align="right">3</td>
+ * <td>2</td>
+ * </tr>
+ * <tr>
+ * <td>1.23</td>
+ * <td>1</td>
+ * <td align="right">23</td>
+ * <td>2</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <p>
+ * An "identifier" is a sequence of characters that do not have the Unicode Pattern_Syntax or Pattern_White_Space
+ * properties.
+ * <p>
+ * The difference between 'in' and 'within' is that 'in' only includes integers in the specified range, while 'within'
+ * includes all values. Using 'within' with a range_list consisting entirely of values is the same as using 'in' (it's
+ * not an error).
+ * </p>
+ *
+ * @stable ICU 3.8
+ */
+public class PluralRules implements Serializable {
+
+ static final UnicodeSet ALLOWED_ID = new UnicodeSet("[a-z]").freeze();
+
+ // TODO Remove RulesList by moving its API and fields into PluralRules.
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static final String CATEGORY_SEPARATOR = "; ";
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static final String KEYWORD_RULE_SEPARATOR = ": ";
+
+ private static final long serialVersionUID = 1;
+
+ private final RuleList rules;
+ private final transient Set<String> keywords;
+
+ /**
+ * Provides a factory for returning plural rules
+ *
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public static abstract class Factory {
+ /**
+ * Provides access to the predefined <code>PluralRules</code> for a given locale and the plural type.
+ *
+ * <p>
+ * ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>. For these predefined
+ * rules, see CLDR page at http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ *
+ * @param locale
+ * The locale for which a <code>PluralRules</code> object is returned.
+ * @param type
+ * The plural type (e.g., cardinal or ordinal).
+ * @return The predefined <code>PluralRules</code> object for this locale. If there's no predefined rules for
+ * this locale, the rules for the closest parent in the locale hierarchy that has one will be returned.
+ * The final fallback always returns the default rules.
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public abstract PluralRules forLocale(ULocale locale, PluralType type);
+
+ /**
+ * Utility for getting CARDINAL rules.
+ * @param locale the locale
+ * @return plural rules.
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public final PluralRules forLocale(ULocale locale) {
+ return forLocale(locale, PluralType.CARDINAL);
+ }
+
+ /**
+ * Returns the locales for which there is plurals data.
+ *
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public abstract ULocale[] getAvailableULocales();
+
+ /**
+ * Returns the 'functionally equivalent' locale with respect to plural rules. Calling PluralRules.forLocale with
+ * the functionally equivalent locale, and with the provided locale, returns rules that behave the same. <br/>
+ * All locales with the same functionally equivalent locale have plural rules that behave the same. This is not
+ * exaustive; there may be other locales whose plural rules behave the same that do not have the same equivalent
+ * locale.
+ *
+ * @param locale
+ * the locale to check
+ * @param isAvailable
+ * if not null and of length > 0, this will hold 'true' at index 0 if locale is directly defined
+ * (without fallback) as having plural rules
+ * @return the functionally-equivalent locale
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public abstract ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable);
+
+ /**
+ * Returns the default factory.
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public static PluralRulesLoader getDefaultFactory() {
+ return PluralRulesLoader.loader;
+ }
+
+ /**
+ * Returns whether or not there are overrides.
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public abstract boolean hasOverride(ULocale locale);
+ }
+ // Standard keywords.
+
+ /**
+ * Common name for the 'zero' plural form.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_ZERO = "zero";
+
+ /**
+ * Common name for the 'singular' plural form.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_ONE = "one";
+
+ /**
+ * Common name for the 'dual' plural form.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_TWO = "two";
+
+ /**
+ * Common name for the 'paucal' or other special plural form.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_FEW = "few";
+
+ /**
+ * Common name for the arabic (11 to 99) plural form.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_MANY = "many";
+
+ /**
+ * Common name for the default plural form. This name is returned
+ * for values to which no other form in the rule applies. It
+ * can additionally be assigned rules of its own.
+ * @stable ICU 3.8
+ */
+ public static final String KEYWORD_OTHER = "other";
+
+ /**
+ * Value returned by {@link #getUniqueKeywordValue} when there is no
+ * unique value to return.
+ * @stable ICU 4.8
+ */
+ public static final double NO_UNIQUE_VALUE = -0.00123456777;
+
+ /**
+ * Type of plurals and PluralRules.
+ * @stable ICU 50
+ */
+ public enum PluralType {
+ /**
+ * Plural rules for cardinal numbers: 1 file vs. 2 files.
+ * @stable ICU 50
+ */
+ CARDINAL,
+ /**
+ * Plural rules for ordinal numbers: 1st file, 2nd file, 3rd file, 4th file, etc.
+ * @stable ICU 50
+ */
+ ORDINAL
+ };
+
+ /*
+ * The default constraint that is always satisfied.
+ */
+ private static final Constraint NO_CONSTRAINT = new Constraint() {
+ private static final long serialVersionUID = 9163464945387899416L;
+
+ public boolean isFulfilled(FixedDecimal n) {
+ return true;
+ }
+
+ public boolean isLimited(SampleType sampleType) {
+ return false;
+ }
+
+ public String toString() {
+ return "";
+ }
+ };
+
+ /**
+ *
+ */
+ private static final Rule DEFAULT_RULE = new Rule("other", NO_CONSTRAINT, null, null);
+
+ /**
+ * Parses a plural rules description and returns a PluralRules.
+ * @param description the rule description.
+ * @throws ParseException if the description cannot be parsed.
+ * The exception index is typically not set, it will be -1.
+ * @stable ICU 3.8
+ */
+ public static PluralRules parseDescription(String description)
+ throws ParseException {
+
+ description = description.trim();
+ return description.length() == 0 ? DEFAULT : new PluralRules(parseRuleChain(description));
+ }
+
+ /**
+ * Creates a PluralRules from a description if it is parsable,
+ * otherwise returns null.
+ * @param description the rule description.
+ * @return the PluralRules
+ * @stable ICU 3.8
+ */
+ public static PluralRules createRules(String description) {
+ try {
+ return parseDescription(description);
+ } catch(Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * The default rules that accept any number and return
+ * {@link #KEYWORD_OTHER}.
+ * @stable ICU 3.8
+ */
+ public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
+
+ private enum Operand {
+ n,
+ i,
+ f,
+ t,
+ v,
+ w,
+ /**@deprecated*/
+ j;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static class FixedDecimal extends Number implements Comparable<FixedDecimal> {
+ private static final long serialVersionUID = -4756200506571685661L;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final double source;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final int visibleDecimalDigitCount;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final int visibleDecimalDigitCountWithoutTrailingZeros;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final long decimalDigits;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final long decimalDigitsWithoutTrailingZeros;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final long integerValue;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final boolean hasIntegerValue;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final boolean isNegative;
+ private final int baseFactor;
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public double getSource() {
+ return source;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public int getVisibleDecimalDigitCount() {
+ return visibleDecimalDigitCount;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public int getVisibleDecimalDigitCountWithoutTrailingZeros() {
+ return visibleDecimalDigitCountWithoutTrailingZeros;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public long getDecimalDigits() {
+ return decimalDigits;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public long getDecimalDigitsWithoutTrailingZeros() {
+ return decimalDigitsWithoutTrailingZeros;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public long getIntegerValue() {
+ return integerValue;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public boolean isHasIntegerValue() {
+ return hasIntegerValue;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public boolean isNegative() {
+ return isNegative;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public int getBaseFactor() {
+ return baseFactor;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ * @param n is the original number
+ * @param v number of digits to the right of the decimal place. e.g 1.00 = 2 25. = 0
+ * @param f Corresponds to f in the plural rules grammar.
+ * The digits to the right of the decimal place as an integer. e.g 1.10 = 10
+ */
+ public FixedDecimal(double n, int v, long f) {
+ isNegative = n < 0;
+ source = isNegative ? -n : n;
+ visibleDecimalDigitCount = v;
+ decimalDigits = f;
+ integerValue = (long)n;
+ hasIntegerValue = source == integerValue;
+ // check values. TODO make into unit test.
+ //
+ // long visiblePower = (int) Math.pow(10, v);
+ // if (fractionalDigits > visiblePower) {
+ // throw new IllegalArgumentException();
+ // }
+ // double fraction = intValue + (fractionalDigits / (double) visiblePower);
+ // if (fraction != source) {
+ // double diff = Math.abs(fraction - source)/(Math.abs(fraction) + Math.abs(source));
+ // if (diff > 0.00000001d) {
+ // throw new IllegalArgumentException();
+ // }
+ // }
+ if (f == 0) {
+ decimalDigitsWithoutTrailingZeros = 0;
+ visibleDecimalDigitCountWithoutTrailingZeros = 0;
+ } else {
+ long fdwtz = f;
+ int trimmedCount = v;
+ while ((fdwtz%10) == 0) {
+ fdwtz /= 10;
+ --trimmedCount;
+ }
+ decimalDigitsWithoutTrailingZeros = fdwtz;
+ visibleDecimalDigitCountWithoutTrailingZeros = trimmedCount;
+ }
+ baseFactor = (int) Math.pow(10, v);
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimal(double n, int v) {
+ this(n,v,getFractionalDigits(n, v));
+ }
+
+ private static int getFractionalDigits(double n, int v) {
+ if (v == 0) {
+ return 0;
+ } else {
+ int baseFactor = (int) Math.pow(10, v);
+ long scaled = Math.round(n * baseFactor);
+ return (int) (scaled % baseFactor);
+ }
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimal(double n) {
+ this(n, decimals(n));
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimal(long n) {
+ this(n,0);
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static int decimals(double n) {
+ // Ugly...
+ String temp = String.valueOf(n);
+ return temp.endsWith(".0") ? 0 : temp.length() - temp.indexOf('.') - 1;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimal (String n) {
+ // Ugly, but for samples we don't care.
+ this(Double.parseDouble(n), getVisibleFractionCount(n));
+ }
+
+ private static int getVisibleFractionCount(String value) {
+ value = value.trim();
+ int decimalPos = value.indexOf('.') + 1;
+ if (decimalPos == 0) {
+ return 0;
+ } else {
+ return value.length() - decimalPos;
+ }
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public double get(Operand operand) {
+ switch(operand) {
+ default: return source;
+ case i: return integerValue;
+ case f: return decimalDigits;
+ case t: return decimalDigitsWithoutTrailingZeros;
+ case v: return visibleDecimalDigitCount;
+ case w: return visibleDecimalDigitCountWithoutTrailingZeros;
+ }
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static Operand getOperand(String t) {
+ return Operand.valueOf(t);
+ }
+
+ /**
+ * We're not going to care about NaN.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public int compareTo(FixedDecimal other) {
+ if (integerValue != other.integerValue) {
+ return integerValue < other.integerValue ? -1 : 1;
+ }
+ if (source != other.source) {
+ return source < other.source ? -1 : 1;
+ }
+ if (visibleDecimalDigitCount != other.visibleDecimalDigitCount) {
+ return visibleDecimalDigitCount < other.visibleDecimalDigitCount ? -1 : 1;
+ }
+ long diff = decimalDigits - other.decimalDigits;
+ if (diff != 0) {
+ return diff < 0 ? -1 : 1;
+ }
+ return 0;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public boolean equals(Object arg0) {
+ if (arg0 == null) {
+ return false;
+ }
+ if (arg0 == this) {
+ return true;
+ }
+ if (!(arg0 instanceof FixedDecimal)) {
+ return false;
+ }
+ FixedDecimal other = (FixedDecimal)arg0;
+ return source == other.source && visibleDecimalDigitCount == other.visibleDecimalDigitCount && decimalDigits == other.decimalDigits;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public int hashCode() {
+ // TODO Auto-generated method stub
+ return (int)(decimalDigits + 37 * (visibleDecimalDigitCount + (int)(37 * source)));
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public String toString() {
+ return String.format("%." + visibleDecimalDigitCount + "f", source);
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public boolean hasIntegerValue() {
+ return hasIntegerValue;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public int intValue() {
+ // TODO Auto-generated method stub
+ return (int)integerValue;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public long longValue() {
+ return integerValue;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public float floatValue() {
+ return (float) source;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public double doubleValue() {
+ return source;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public long getShiftedValue() {
+ return integerValue * baseFactor + decimalDigits;
+ }
+
+ private void writeObject(
+ ObjectOutputStream out)
+ throws IOException {
+ throw new NotSerializableException();
+ }
+
+ private void readObject(ObjectInputStream in
+ ) throws IOException, ClassNotFoundException {
+ throw new NotSerializableException();
+ }
+ }
+
+ /**
+ * Selection parameter for either integer-only or decimal-only.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public enum SampleType {INTEGER, DECIMAL}
+
+ /**
+ * A range of NumberInfo that includes all values with the same visibleFractionDigitCount.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static class FixedDecimalRange {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final FixedDecimal start;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final FixedDecimal end;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimalRange(FixedDecimal start, FixedDecimal end) {
+ if (start.visibleDecimalDigitCount != end.visibleDecimalDigitCount) {
+ throw new IllegalArgumentException("Ranges must have the same number of visible decimals: " + start + "~" + end);
+ }
+ this.start = start;
+ this.end = end;
+ }
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public String toString() {
+ return start + (end == start ? "" : "~" + end);
+ }
+ }
+
+ /**
+ * A list of NumberInfo that includes all values with the same visibleFractionDigitCount.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public static class FixedDecimalSamples {
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final SampleType sampleType;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final Set<FixedDecimalRange> samples;
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public final boolean bounded;
+ /**
+ * The samples must be immutable.
+ * @param sampleType
+ * @param samples
+ */
+ private FixedDecimalSamples(SampleType sampleType, Set<FixedDecimalRange> samples, boolean bounded) {
+ super();
+ this.sampleType = sampleType;
+ this.samples = samples;
+ this.bounded = bounded;
+ }
+ /*
+ * Parse a list of the form described in CLDR. The source must be trimmed.
+ */
+ static FixedDecimalSamples parse(String source) {
+ SampleType sampleType2;
+ boolean bounded2 = true;
+ boolean haveBound = false;
+ Set<FixedDecimalRange> samples2 = new LinkedHashSet<FixedDecimalRange>();
+
+ if (source.startsWith("integer")) {
+ sampleType2 = SampleType.INTEGER;
+ } else if (source.startsWith("decimal")) {
+ sampleType2 = SampleType.DECIMAL;
+ } else {
+ throw new IllegalArgumentException("Samples must start with 'integer' or 'decimal'");
+ }
+ source = source.substring(7).trim(); // remove both
+
+ for (String range : COMMA_SEPARATED.split(source)) {
+ if (range.equals("…") || range.equals("...")) {
+ bounded2 = false;
+ haveBound = true;
+ continue;
+ }
+ if (haveBound) {
+ throw new IllegalArgumentException("Can only have … at the end of samples: " + range);
+ }
+ String[] rangeParts = TILDE_SEPARATED.split(range);
+ switch (rangeParts.length) {
+ case 1:
+ FixedDecimal sample = new FixedDecimal(rangeParts[0]);
+ checkDecimal(sampleType2, sample);
+ samples2.add(new FixedDecimalRange(sample, sample));
+ break;
+ case 2:
+ FixedDecimal start = new FixedDecimal(rangeParts[0]);
+ FixedDecimal end = new FixedDecimal(rangeParts[1]);
+ checkDecimal(sampleType2, start);
+ checkDecimal(sampleType2, end);
+ samples2.add(new FixedDecimalRange(start, end));
+ break;
+ default: throw new IllegalArgumentException("Ill-formed number range: " + range);
+ }
+ }
+ return new FixedDecimalSamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2);
+ }
+
+ private static void checkDecimal(SampleType sampleType2, FixedDecimal sample) {
+ if ((sampleType2 == SampleType.INTEGER) != (sample.getVisibleDecimalDigitCount() == 0)) {
+ throw new IllegalArgumentException("Ill-formed number range: " + sample);
+ }
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public Set<Double> addSamples(Set<Double> result) {
+ for (FixedDecimalRange item : samples) {
+ // we have to convert to longs so we don't get strange double issues
+ long startDouble = item.start.getShiftedValue();
+ long endDouble = item.end.getShiftedValue();
+
+ for (long d = startDouble; d <= endDouble; d += 1) {
+ result.add(d/(double)item.start.baseFactor);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder("@").append(sampleType.toString().toLowerCase(Locale.ENGLISH));
+ boolean first = true;
+ for (FixedDecimalRange item : samples) {
+ if (first) {
+ first = false;
+ } else {
+ b.append(",");
+ }
+ b.append(' ').append(item);
+ }
+ if (!bounded) {
+ b.append(", …");
+ }
+ return b.toString();
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public Set<FixedDecimalRange> getSamples() {
+ return samples;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public void getStartEndSamples(Set<FixedDecimal> target) {
+ for (FixedDecimalRange item : samples) {
+ target.add(item.start);
+ target.add(item.end);
+ }
+ }
+ }
+
+ /*
+ * A constraint on a number.
+ */
+ private interface Constraint extends Serializable {
+ /*
+ * Returns true if the number fulfills the constraint.
+ * @param n the number to test, >= 0.
+ */
+ boolean isFulfilled(FixedDecimal n);
+
+ /*
+ * Returns false if an unlimited number of values fulfills the
+ * constraint.
+ */
+ boolean isLimited(SampleType sampleType);
+ }
+
+ static class SimpleTokenizer {
+ static final UnicodeSet BREAK_AND_IGNORE = new UnicodeSet(0x09, 0x0a, 0x0c, 0x0d, 0x20, 0x20).freeze();
+ static final UnicodeSet BREAK_AND_KEEP = new UnicodeSet('!', '!', '%', '%', ',', ',', '.', '.', '=', '=').freeze();
+ static String[] split(String source) {
+ int last = -1;
+ List<String> result = new ArrayList<String>();
+ for (int i = 0; i < source.length(); ++i) {
+ char ch = source.charAt(i);
+ if (BREAK_AND_IGNORE.contains(ch)) {
+ if (last >= 0) {
+ result.add(source.substring(last,i));
+ last = -1;
+ }
+ } else if (BREAK_AND_KEEP.contains(ch)) {
+ if (last >= 0) {
+ result.add(source.substring(last,i));
+ }
+ result.add(source.substring(i,i+1));
+ last = -1;
+ } else if (last < 0) {
+ last = i;
+ }
+ }
+ if (last >= 0) {
+ result.add(source.substring(last));
+ }
+ return result.toArray(new String[result.size()]);
+ }
+ }
+
+ /*
+ * syntax:
+ * condition : or_condition
+ * and_condition
+ * or_condition : and_condition 'or' condition
+ * and_condition : relation
+ * relation 'and' relation
+ * relation : in_relation
+ * within_relation
+ * in_relation : not? expr not? in not? range
+ * within_relation : not? expr not? 'within' not? range
+ * not : 'not'
+ * '!'
+ * expr : 'n'
+ * 'n' mod value
+ * mod : 'mod'
+ * '%'
+ * in : 'in'
+ * 'is'
+ * '='
+ * '≠'
+ * value : digit+
+ * digit : 0|1|2|3|4|5|6|7|8|9
+ * range : value'..'value
+ */
+ private static Constraint parseConstraint(String description)
+ throws ParseException {
+
+ Constraint result = null;
+ String[] or_together = OR_SEPARATED.split(description);
+ for (int i = 0; i < or_together.length; ++i) {
+ Constraint andConstraint = null;
+ String[] and_together = AND_SEPARATED.split(or_together[i]);
+ for (int j = 0; j < and_together.length; ++j) {
+ Constraint newConstraint = NO_CONSTRAINT;
+
+ String condition = and_together[j].trim();
+ String[] tokens = SimpleTokenizer.split(condition);
+
+ int mod = 0;
+ boolean inRange = true;
+ boolean integersOnly = true;
+ double lowBound = Long.MAX_VALUE;
+ double highBound = Long.MIN_VALUE;
+ long[] vals = null;
+
+ int x = 0;
+ String t = tokens[x++];
+ boolean hackForCompatibility = false;
+ Operand operand;
+ try {
+ operand = FixedDecimal.getOperand(t);
+ } catch (Exception e) {
+ throw unexpected(t, condition);
+ }
+ if (x < tokens.length) {
+ t = tokens[x++];
+ if ("mod".equals(t) || "%".equals(t)) {
+ mod = Integer.parseInt(tokens[x++]);
+ t = nextToken(tokens, x++, condition);
+ }
+ if ("not".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ if ("=".equals(t)) {
+ throw unexpected(t, condition);
+ }
+ } else if ("!".equals(t)) {
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ if (!"=".equals(t)) {
+ throw unexpected(t, condition);
+ }
+ }
+ if ("is".equals(t) || "in".equals(t) || "=".equals(t)) {
+ hackForCompatibility = "is".equals(t);
+ if (hackForCompatibility && !inRange) {
+ throw unexpected(t, condition);
+ }
+ t = nextToken(tokens, x++, condition);
+ } else if ("within".equals(t)) {
+ integersOnly = false;
+ t = nextToken(tokens, x++, condition);
+ } else {
+ throw unexpected(t, condition);
+ }
+ if ("not".equals(t)) {
+ if (!hackForCompatibility && !inRange) {
+ throw unexpected(t, condition);
+ }
+ inRange = !inRange;
+ t = nextToken(tokens, x++, condition);
+ }
+
+ List<Long> valueList = new ArrayList<Long>();
+
+ // the token t is always one item ahead
+ while (true) {
+ long low = Long.parseLong(t);
+ long high = low;
+ if (x < tokens.length) {
+ t = nextToken(tokens, x++, condition);
+ if (t.equals(".")) {
+ t = nextToken(tokens, x++, condition);
+ if (!t.equals(".")) {
+ throw unexpected(t, condition);
+ }
+ t = nextToken(tokens, x++, condition);
+ high = Long.parseLong(t);
+ if (x < tokens.length) {
+ t = nextToken(tokens, x++, condition);
+ if (!t.equals(",")) { // adjacent number: 1 2
+ // no separator, fail
+ throw unexpected(t, condition);
+ }
+ }
+ } else if (!t.equals(",")) { // adjacent number: 1 2
+ // no separator, fail
+ throw unexpected(t, condition);
+ }
+ }
+ // at this point, either we are out of tokens, or t is ','
+ if (low > high) {
+ throw unexpected(low + "~" + high, condition);
+ } else if (mod != 0 && high >= mod) {
+ throw unexpected(high + ">mod=" + mod, condition);
+ }
+ valueList.add(low);
+ valueList.add(high);
+ lowBound = Math.min(lowBound, low);
+ highBound = Math.max(highBound, high);
+ if (x >= tokens.length) {
+ break;
+ }
+ t = nextToken(tokens, x++, condition);
+ }
+
+ if (t.equals(",")) {
+ throw unexpected(t, condition);
+ }
+
+ if (valueList.size() == 2) {
+ vals = null;
+ } else {
+ vals = new long[valueList.size()];
+ for (int k = 0; k < vals.length; ++k) {
+ vals[k] = valueList.get(k);
+ }
+ }
+
+ // Hack to exclude "is not 1,2"
+ if (lowBound != highBound && hackForCompatibility && !inRange) {
+ throw unexpected("is not <range>", condition);
+ }
+
+ newConstraint =
+ new RangeConstraint(mod, inRange, operand, integersOnly, lowBound, highBound, vals);
+ }
+
+ if (andConstraint == null) {
+ andConstraint = newConstraint;
+ } else {
+ andConstraint = new AndConstraint(andConstraint,
+ newConstraint);
+ }
+ }
+
+ if (result == null) {
+ result = andConstraint;
+ } else {
+ result = new OrConstraint(result, andConstraint);
+ }
+ }
+ return result;
+ }
+
+ static final Pattern AT_SEPARATED = Pattern.compile("\\s*\\Q\\E@\\s*");
+ static final Pattern OR_SEPARATED = Pattern.compile("\\s*or\\s*");
+ static final Pattern AND_SEPARATED = Pattern.compile("\\s*and\\s*");
+ static final Pattern COMMA_SEPARATED = Pattern.compile("\\s*,\\s*");
+ static final Pattern DOTDOT_SEPARATED = Pattern.compile("\\s*\\Q..\\E\\s*");
+ static final Pattern TILDE_SEPARATED = Pattern.compile("\\s*~\\s*");
+ static final Pattern SEMI_SEPARATED = Pattern.compile("\\s*;\\s*");
+
+
+ /* Returns a parse exception wrapping the token and context strings. */
+ private static ParseException unexpected(String token, String context) {
+ return new ParseException("unexpected token '" + token +
+ "' in '" + context + "'", -1);
+ }
+
+ /*
+ * Returns the token at x if available, else throws a parse exception.
+ */
+ private static String nextToken(String[] tokens, int x, String context)
+ throws ParseException {
+ if (x < tokens.length) {
+ return tokens[x];
+ }
+ throw new ParseException("missing token at end of '" + context + "'", -1);
+ }
+
+ /*
+ * Syntax:
+ * rule : keyword ':' condition
+ * keyword: <identifier>
+ */
+ private static Rule parseRule(String description) throws ParseException {
+ if (description.length() == 0) {
+ return DEFAULT_RULE;
+ }
+
+ description = description.toLowerCase(Locale.ENGLISH);
+
+ int x = description.indexOf(':');
+ if (x == -1) {
+ throw new ParseException("missing ':' in rule description '" +
+ description + "'", 0);
+ }
+
+ String keyword = description.substring(0, x).trim();
+ if (!isValidKeyword(keyword)) {
+ throw new ParseException("keyword '" + keyword +
+ " is not valid", 0);
+ }
+
+ description = description.substring(x+1).trim();
+ String[] constraintOrSamples = AT_SEPARATED.split(description);
+ boolean sampleFailure = false;
+ FixedDecimalSamples integerSamples = null, decimalSamples = null;
+ switch (constraintOrSamples.length) {
+ case 1: break;
+ case 2:
+ integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
+ if (integerSamples.sampleType == SampleType.DECIMAL) {
+ decimalSamples = integerSamples;
+ integerSamples = null;
+ }
+ break;
+ case 3:
+ integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
+ decimalSamples = FixedDecimalSamples.parse(constraintOrSamples[2]);
+ if (integerSamples.sampleType != SampleType.INTEGER || decimalSamples.sampleType != SampleType.DECIMAL) {
+ throw new IllegalArgumentException("Must have @integer then @decimal in " + description);
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Too many samples in " + description);
+ }
+ if (sampleFailure) {
+ throw new IllegalArgumentException("Ill-formed samples—'@' characters.");
+ }
+
+ // 'other' is special, and must have no rules; all other keywords must have rules.
+ boolean isOther = keyword.equals("other");
+ if (isOther != (constraintOrSamples[0].length() == 0)) {
+ throw new IllegalArgumentException("The keyword 'other' must have no constraints, just samples.");
+ }
+
+ Constraint constraint;
+ if (isOther) {
+ constraint = NO_CONSTRAINT;
+ } else {
+ constraint = parseConstraint(constraintOrSamples[0]);
+ }
+ return new Rule(keyword, constraint, integerSamples, decimalSamples);
+ }
+
+
+ /*
+ * Syntax:
+ * rules : rule
+ * rule ';' rules
+ */
+ private static RuleList parseRuleChain(String description)
+ throws ParseException {
+ RuleList result = new RuleList();
+ // remove trailing ;
+ if (description.endsWith(";")) {
+ description = description.substring(0,description.length()-1);
+ }
+ String[] rules = SEMI_SEPARATED.split(description);
+ for (int i = 0; i < rules.length; ++i) {
+ Rule rule = parseRule(rules[i].trim());
+ result.hasExplicitBoundingInfo |= rule.integerSamples != null || rule.decimalSamples != null;
+ result.addRule(rule);
+ }
+ return result.finish();
+ }
+
+ /*
+ * An implementation of Constraint representing a modulus,
+ * a range of values, and include/exclude. Provides lots of
+ * convenience factory methods.
+ */
+ private static class RangeConstraint implements Constraint, Serializable {
+ private static final long serialVersionUID = 1;
+
+ private final int mod;
+ private final boolean inRange;
+ private final boolean integersOnly;
+ private final double lowerBound;
+ private final double upperBound;
+ private final long[] range_list;
+ private final Operand operand;
+
+ RangeConstraint(int mod, boolean inRange, Operand operand, boolean integersOnly,
+ double lowBound, double highBound, long[] vals) {
+ this.mod = mod;
+ this.inRange = inRange;
+ this.integersOnly = integersOnly;
+ this.lowerBound = lowBound;
+ this.upperBound = highBound;
+ this.range_list = vals;
+ this.operand = operand;
+ }
+
+ public boolean isFulfilled(FixedDecimal number) {
+ double n = number.get(operand);
+ if ((integersOnly && (n - (long)n) != 0.0
+ || operand == Operand.j && number.visibleDecimalDigitCount != 0)) {
+ return !inRange;
+ }
+ if (mod != 0) {
+ n = n % mod; // java % handles double numerator the way we want
+ }
+ boolean test = n >= lowerBound && n <= upperBound;
+ if (test && range_list != null) {
+ test = false;
+ for (int i = 0; !test && i < range_list.length; i += 2) {
+ test = n >= range_list[i] && n <= range_list[i+1];
+ }
+ }
+ return inRange == test;
+ }
+
+ public boolean isLimited(SampleType sampleType) {
+ boolean valueIsZero = lowerBound == upperBound && lowerBound == 0d;
+ boolean hasDecimals =
+ (operand == Operand.v || operand == Operand.w || operand == Operand.f || operand == Operand.t)
+ && inRange != valueIsZero; // either NOT f = zero or f = non-zero
+ switch (sampleType) {
+ case INTEGER:
+ return hasDecimals // will be empty
+ || (operand == Operand.n || operand == Operand.i || operand == Operand.j)
+ && mod == 0
+ && inRange;
+
+ case DECIMAL:
+ return (!hasDecimals || operand == Operand.n || operand == Operand.j)
+ && (integersOnly || lowerBound == upperBound)
+ && mod == 0
+ && inRange;
+ }
+ return false;
+ }
+
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(operand);
+ if (mod != 0) {
+ result.append(" % ").append(mod);
+ }
+ boolean isList = lowerBound != upperBound;
+ result.append(
+ !isList ? (inRange ? " = " : " != ")
+ : integersOnly ? (inRange ? " = " : " != ")
+ : (inRange ? " within " : " not within ")
+ );
+ if (range_list != null) {
+ for (int i = 0; i < range_list.length; i += 2) {
+ addRange(result, range_list[i], range_list[i+1], i != 0);
+ }
+ } else {
+ addRange(result, lowerBound, upperBound, false);
+ }
+ return result.toString();
+ }
+ }
+
+ private static void addRange(StringBuilder result, double lb, double ub, boolean addSeparator) {
+ if (addSeparator) {
+ result.append(",");
+ }
+ if (lb == ub) {
+ result.append(format(lb));
+ } else {
+ result.append(format(lb) + ".." + format(ub));
+ }
+ }
+
+ private static String format(double lb) {
+ long lbi = (long) lb;
+ return lb == lbi ? String.valueOf(lbi) : String.valueOf(lb);
+ }
+
+ /* Convenience base class for and/or constraints. */
+ private static abstract class BinaryConstraint implements Constraint,
+ Serializable {
+ private static final long serialVersionUID = 1;
+ protected final Constraint a;
+ protected final Constraint b;
+
+ protected BinaryConstraint(Constraint a, Constraint b) {
+ this.a = a;
+ this.b = b;
+ }
+ }
+
+ /* A constraint representing the logical and of two constraints. */
+ private static class AndConstraint extends BinaryConstraint {
+ private static final long serialVersionUID = 7766999779862263523L;
+
+ AndConstraint(Constraint a, Constraint b) {
+ super(a, b);
+ }
+
+ public boolean isFulfilled(FixedDecimal n) {
+ return a.isFulfilled(n)
+ && b.isFulfilled(n);
+ }
+
+ public boolean isLimited(SampleType sampleType) {
+ // we ignore the case where both a and b are unlimited but no values
+ // satisfy both-- we still consider this 'unlimited'
+ return a.isLimited(sampleType)
+ || b.isLimited(sampleType);
+ }
+
+ public String toString() {
+ return a.toString() + " and " + b.toString();
+ }
+ }
+
+ /* A constraint representing the logical or of two constraints. */
+ private static class OrConstraint extends BinaryConstraint {
+ private static final long serialVersionUID = 1405488568664762222L;
+
+ OrConstraint(Constraint a, Constraint b) {
+ super(a, b);
+ }
+
+ public boolean isFulfilled(FixedDecimal n) {
+ return a.isFulfilled(n)
+ || b.isFulfilled(n);
+ }
+
+ public boolean isLimited(SampleType sampleType) {
+ return a.isLimited(sampleType)
+ && b.isLimited(sampleType);
+ }
+
+ public String toString() {
+ return a.toString() + " or " + b.toString();
+ }
+ }
+
+ /*
+ * Implementation of Rule that uses a constraint.
+ * Provides 'and' and 'or' to combine constraints. Immutable.
+ */
+ private static class Rule implements Serializable {
+ private static final long serialVersionUID = 1;
+ private final String keyword;
+ private final Constraint constraint;
+ private final FixedDecimalSamples integerSamples;
+ private final FixedDecimalSamples decimalSamples;
+
+ public Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples) {
+ this.keyword = keyword;
+ this.constraint = constraint;
+ this.integerSamples = integerSamples;
+ this.decimalSamples = decimalSamples;
+ }
+
+ @SuppressWarnings("unused")
+ public Rule and(Constraint c) {
+ return new Rule(keyword, new AndConstraint(constraint, c), integerSamples, decimalSamples);
+ }
+
+ @SuppressWarnings("unused")
+ public Rule or(Constraint c) {
+ return new Rule(keyword, new OrConstraint(constraint, c), integerSamples, decimalSamples);
+ }
+
+ public String getKeyword() {
+ return keyword;
+ }
+
+ public boolean appliesTo(FixedDecimal n) {
+ return constraint.isFulfilled(n);
+ }
+
+ public boolean isLimited(SampleType sampleType) {
+ return constraint.isLimited(sampleType);
+ }
+
+ public String toString() {
+ return keyword + ": " + constraint.toString()
+ + (integerSamples == null ? "" : " " + integerSamples.toString())
+ + (decimalSamples == null ? "" : " " + decimalSamples.toString());
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public int hashCode() {
+ return keyword.hashCode() ^ constraint.hashCode();
+ }
+
+ public String getConstraint() {
+ return constraint.toString();
+ }
+ }
+
+ private static class RuleList implements Serializable {
+ private boolean hasExplicitBoundingInfo = false;
+ private static final long serialVersionUID = 1;
+ private final List<Rule> rules = new ArrayList<Rule>();
+
+ public RuleList addRule(Rule nextRule) {
+ String keyword = nextRule.getKeyword();
+ for (Rule rule : rules) {
+ if (keyword.equals(rule.getKeyword())) {
+ throw new IllegalArgumentException("Duplicate keyword: " + keyword);
+ }
+ }
+ rules.add(nextRule);
+ return this;
+ }
+
+ public RuleList finish() throws ParseException {
+ // make sure that 'other' is present, and at the end.
+ Rule otherRule = null;
+ for (Iterator<Rule> it = rules.iterator(); it.hasNext();) {
+ Rule rule = it.next();
+ if ("other".equals(rule.getKeyword())) {
+ otherRule = rule;
+ it.remove();
+ }
+ }
+ if (otherRule == null) {
+ otherRule = parseRule("other:"); // make sure we have always have an 'other' a rule
+ }
+ rules.add(otherRule);
+ return this;
+ }
+
+ private Rule selectRule(FixedDecimal n) {
+ for (Rule rule : rules) {
+ if (rule.appliesTo(n)) {
+ return rule;
+ }
+ }
+ return null;
+ }
+
+ public String select(FixedDecimal n) {
+ Rule r = selectRule(n);
+ // since we have explict 'other', we don't need this.
+ // if (r == null) {
+ // return KEYWORD_OTHER;
+ // }
+ return r.getKeyword();
+ }
+
+ public Set<String> getKeywords() {
+ Set<String> result = new LinkedHashSet<String>();
+ for (Rule rule : rules) {
+ result.add(rule.getKeyword());
+ }
+ // since we have explict 'other', we don't need this.
+ //result.add(KEYWORD_OTHER);
+ return result;
+ }
+
+ public boolean isLimited(String keyword, SampleType sampleType) {
+ if (hasExplicitBoundingInfo) {
+ FixedDecimalSamples mySamples = getDecimalSamples(keyword, sampleType);
+ return mySamples == null ? true : mySamples.bounded;
+ }
+
+ return computeLimited(keyword, sampleType);
+ }
+
+ public boolean computeLimited(String keyword, SampleType sampleType) {
+ // if all rules with this keyword are limited, it's limited,
+ // and if there's no rule with this keyword, it's unlimited
+ boolean result = false;
+ for (Rule rule : rules) {
+ if (keyword.equals(rule.getKeyword())) {
+ if (!rule.isLimited(sampleType)) {
+ return false;
+ }
+ result = true;
+ }
+ }
+ return result;
+ }
+
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ for (Rule rule : rules) {
+ if (builder.length() != 0) {
+ builder.append(CATEGORY_SEPARATOR);
+ }
+ builder.append(rule);
+ }
+ return builder.toString();
+ }
+
+ public String getRules(String keyword) {
+ for (Rule rule : rules) {
+ if (rule.getKeyword().equals(keyword)) {
+ return rule.getConstraint();
+ }
+ }
+ return null;
+ }
+
+ public boolean select(FixedDecimal sample, String keyword) {
+ for (Rule rule : rules) {
+ if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+ for (Rule rule : rules) {
+ if (rule.getKeyword().equals(keyword)) {
+ return sampleType == SampleType.INTEGER ? rule.integerSamples : rule.decimalSamples;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * @deprecated This API is ICU internal only.
+ * @internal
+ */
+ public enum StandardPluralCategories {
+ zero,
+ one,
+ two,
+ few,
+ many,
+ other;
+ static StandardPluralCategories forString(String s) {
+ StandardPluralCategories a;
+ try {
+ a = valueOf(s);
+ } catch (Exception e) {
+ return null;
+ }
+ return a;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) {
+ boolean added;
+ FixedDecimal toAdd = new FixedDecimal(trial);
+ if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
+ others.add(toAdd);
+ added = true;
+ } else {
+ added = false;
+ }
+ return added;
+ }
+
+
+
+ // -------------------------------------------------------------------------
+ // Static class methods.
+ // -------------------------------------------------------------------------
+
+ /**
+ * Provides access to the predefined cardinal-number <code>PluralRules</code> for a given
+ * locale.
+ * Same as forLocale(locale, PluralType.CARDINAL).
+ *
+ * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>.
+ * For these predefined rules, see CLDR page at
+ * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ *
+ * @param locale The locale for which a <code>PluralRules</code> object is
+ * returned.
+ * @return The predefined <code>PluralRules</code> object for this locale.
+ * If there's no predefined rules for this locale, the rules
+ * for the closest parent in the locale hierarchy that has one will
+ * be returned. The final fallback always returns the default
+ * rules.
+ * @stable ICU 3.8
+ */
+ public static PluralRules forLocale(ULocale locale) {
+ return Factory.getDefaultFactory().forLocale(locale, PluralType.CARDINAL);
+ }
+
+ /**
+ * Provides access to the predefined <code>PluralRules</code> for a given
+ * locale and the plural type.
+ *
+ * <p>ICU defines plural rules for many locales based on CLDR <i>Language Plural Rules</i>.
+ * For these predefined rules, see CLDR page at
+ * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ *
+ * @param locale The locale for which a <code>PluralRules</code> object is
+ * returned.
+ * @param type The plural type (e.g., cardinal or ordinal).
+ * @return The predefined <code>PluralRules</code> object for this locale.
+ * If there's no predefined rules for this locale, the rules
+ * for the closest parent in the locale hierarchy that has one will
+ * be returned. The final fallback always returns the default
+ * rules.
+ * @stable ICU 50
+ */
+ public static PluralRules forLocale(ULocale locale, PluralType type) {
+ return Factory.getDefaultFactory().forLocale(locale, type);
+ }
+
+ /*
+ * Checks whether a token is a valid keyword.
+ *
+ * @param token the token to be checked
+ * @return true if the token is a valid keyword.
+ */
+ private static boolean isValidKeyword(String token) {
+ return ALLOWED_ID.containsAll(token);
+ }
+
+ /*
+ * Creates a new <code>PluralRules</code> object. Immutable.
+ */
+ private PluralRules(RuleList rules) {
+ this.rules = rules;
+ this.keywords = Collections.unmodifiableSet(rules.getKeywords());
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ @Override
+ public int hashCode() {
+ return rules.hashCode();
+ }
+ /**
+ * Given a number, returns the keyword of the first rule that applies to
+ * the number.
+ *
+ * @param number The number for which the rule has to be determined.
+ * @return The keyword of the selected rule.
+ * @stable ICU 4.0
+ */
+ public String select(double number) {
+ return rules.select(new FixedDecimal(number));
+ }
+
+ /**
+ * Given a number, returns the keyword of the first rule that applies to
+ * the number.
+ *
+ * @param number The number for which the rule has to be determined.
+ * @return The keyword of the selected rule.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public String select(double number, int countVisibleFractionDigits, long fractionaldigits) {
+ return rules.select(new FixedDecimal(number, countVisibleFractionDigits, fractionaldigits));
+ }
+
+ /**
+ * Given a number information, returns the keyword of the first rule that applies to
+ * the number.
+ *
+ * @param sample The number information for which the rule has to be determined.
+ * @return The keyword of the selected rule.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public String select(FixedDecimal sample) {
+ return rules.select(sample);
+ }
+
+
+ /**
+ * Given a number information, and keyword, return whether the keyword would match the number.
+ *
+ * @param sample The number information for which the rule has to be determined.
+ * @param keyword The keyword to filter on
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public boolean matches(FixedDecimal sample, String keyword) {
+ return rules.select(sample, keyword);
+ }
+
+ /**
+ * Returns a set of all rule keywords used in this <code>PluralRules</code>
+ * object. The rule "other" is always present by default.
+ *
+ * @return The set of keywords.
+ * @stable ICU 3.8
+ */
+ public Set<String> getKeywords() {
+ return keywords;
+ }
+
+ /**
+ * Returns the unique value that this keyword matches, or {@link #NO_UNIQUE_VALUE}
+ * if the keyword matches multiple values or is not defined for this PluralRules.
+ *
+ * @param keyword the keyword to check for a unique value
+ * @return The unique value for the keyword, or NO_UNIQUE_VALUE.
+ * @stable ICU 4.8
+ */
+ public double getUniqueKeywordValue(String keyword) {
+ Collection<Double> values = getAllKeywordValues(keyword);
+ if (values != null && values.size() == 1) {
+ return values.iterator().next();
+ }
+ return NO_UNIQUE_VALUE;
+ }
+
+ /**
+ * Returns all the values that trigger this keyword, or null if the number of such
+ * values is unlimited.
+ *
+ * @param keyword the keyword
+ * @return the values that trigger this keyword, or null. The returned collection
+ * is immutable. It will be empty if the keyword is not defined.
+ * @stable ICU 4.8
+ */
+ public Collection<Double> getAllKeywordValues(String keyword) {
+ return getAllKeywordValues(keyword, SampleType.INTEGER);
+ }
+
+ /**
+ * Returns all the values that trigger this keyword, or null if the number of such
+ * values is unlimited.
+ *
+ * @param keyword the keyword
+ * @return the values that trigger this keyword, or null. The returned collection
+ * is immutable. It will be empty if the keyword is not defined.
+ *
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public Collection<Double> getAllKeywordValues(String keyword, SampleType type) {
+ if (!isLimited(keyword, type)) {
+ return null;
+ }
+ Collection<Double> samples = getSamples(keyword, type);
+ return samples == null ? null : Collections.unmodifiableCollection(samples);
+ }
+
+ /**
+ * Returns a list of values for which select() would return that keyword,
+ * or null if the keyword is not defined. The returned collection is unmodifiable.
+ * The returned list is not complete, and there might be additional values that
+ * would return the keyword.
+ *
+ * @param keyword the keyword to test
+ * @return a list of values matching the keyword.
+ * @stable ICU 4.8
+ */
+ public Collection<Double> getSamples(String keyword) {
+ return getSamples(keyword, SampleType.INTEGER);
+ }
+
+ /**
+ * Returns a list of values for which select() would return that keyword,
+ * or null if the keyword is not defined.
+ * The returned collection is unmodifiable.
+ * The returned list is not complete, and there might be additional values that
+ * would return the keyword. The keyword might be defined, and yet have an empty set of samples,
+ * IF there are samples for the other sampleType.
+ *
+ * @param keyword the keyword to test
+ * @return a list of values matching the keyword.
+ * @deprecated ICU internal only
+ * @internal
+ */
+ public Collection<Double> getSamples(String keyword, SampleType sampleType) {
+ if (!keywords.contains(keyword)) {
+ return null;
+ }
+ Set<Double> result = new TreeSet<Double>();
+
+ if (rules.hasExplicitBoundingInfo) {
+ FixedDecimalSamples samples = rules.getDecimalSamples(keyword, sampleType);
+ return samples == null ? Collections.unmodifiableSet(result)
+ : Collections.unmodifiableSet(samples.addSamples(result));
+ }
+
+ // hack in case the rule is created without explicit samples
+ int maxCount = isLimited(keyword, sampleType) ? Integer.MAX_VALUE : 20;
+
+ switch (sampleType) {
+ case INTEGER:
+ for (int i = 0; i < 200; ++i) {
+ if (!addSample(keyword, i, maxCount, result)) {
+ break;
+ }
+ }
+ addSample(keyword, 1000000, maxCount, result); // hack for Welsh
+ break;
+ case DECIMAL:
+ for (int i = 0; i < 2000; ++i) {
+ if (!addSample(keyword, new FixedDecimal(i/10d, 1), maxCount, result)) {
+ break;
+ }
+ }
+ addSample(keyword, new FixedDecimal(1000000d, 1), maxCount, result); // hack for Welsh
+ break;
+ }
+ return result.size() == 0 ? null : Collections.unmodifiableSet(result);
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) {
+ String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue());
+ if (selectedKeyword.equals(keyword)) {
+ result.add(sample.doubleValue());
+ if (--maxCount < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns a list of values for which select() would return that keyword,
+ * or null if the keyword is not defined or no samples are available.
+ * The returned collection is unmodifiable.
+ * The returned list is not complete, and there might be additional values that
+ * would return the keyword.
+ *
+ * @param keyword the keyword to test
+ * @return a list of values matching the keyword.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+ return rules.getDecimalSamples(keyword, sampleType);
+ }
+
+ /**
+ * Returns the set of locales for which PluralRules are known.
+ * @return the set of locales for which PluralRules are known, as a list
+ * @draft ICU 4.2
+ * @provisional This API might change or be removed in a future release.
+ */
+ public static ULocale[] getAvailableULocales() {
+ return Factory.getDefaultFactory().getAvailableULocales();
+ }
+
+ /**
+ * Returns the 'functionally equivalent' locale with respect to
+ * plural rules. Calling PluralRules.forLocale with the functionally equivalent
+ * locale, and with the provided locale, returns rules that behave the same.
+ * <br/>
+ * All locales with the same functionally equivalent locale have
+ * plural rules that behave the same. This is not exaustive;
+ * there may be other locales whose plural rules behave the same
+ * that do not have the same equivalent locale.
+ *
+ * @param locale the locale to check
+ * @param isAvailable if not null and of length > 0, this will hold 'true' at
+ * index 0 if locale is directly defined (without fallback) as having plural rules
+ * @return the functionally-equivalent locale
+ * @draft ICU 4.2
+ * @provisional This API might change or be removed in a future release.
+ */
+ public static ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
+ return Factory.getDefaultFactory().getFunctionalEquivalent(locale, isAvailable);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 3.8
+ */
+ public String toString() {
+ return rules.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @stable ICU 3.8
+ */
+ public boolean equals(Object rhs) {
+ return rhs instanceof PluralRules && equals((PluralRules)rhs);
+ }
+
+ /**
+ * Returns true if rhs is equal to this.
+ * @param rhs the PluralRules to compare to.
+ * @return true if this and rhs are equal.
+ * @stable ICU 3.8
+ */
+ // TODO Optimize this
+ public boolean equals(PluralRules rhs) {
+ return rhs != null && toString().equals(rhs.toString());
+ }
+
+ /**
+ * Status of the keyword for the rules, given a set of explicit values.
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ public enum KeywordStatus {
+ /**
+ * The keyword is not valid for the rules.
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ INVALID,
+ /**
+ * The keyword is valid, but unused (it is covered by the explicit values, OR has no values for the given {@link SampleType}).
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ SUPPRESSED,
+ /**
+ * The keyword is valid, used, and has a single possible value (before considering explicit values).
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ UNIQUE,
+ /**
+ * The keyword is valid, used, not unique, and has a finite set of values.
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ BOUNDED,
+ /**
+ * The keyword is valid but not bounded; there indefinitely many matching values.
+ *
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ UNBOUNDED
+ }
+
+ /**
+ * Find the status for the keyword, given a certain set of explicit values.
+ *
+ * @param keyword
+ * the particular keyword (call rules.getKeywords() to get the valid ones)
+ * @param offset
+ * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
+ * checking against the keyword values.
+ * @param explicits
+ * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+ * @param uniqueValue
+ * If non null, set to the unique value.
+ * @return the KeywordStatus
+ * @draft ICU 50
+ * @provisional This API might change or be removed in a future release.
+ */
+ public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
+ Output<Double> uniqueValue) {
+ return getKeywordStatus(keyword, offset, explicits, uniqueValue, SampleType.INTEGER);
+ }
+ /**
+ * Find the status for the keyword, given a certain set of explicit values.
+ *
+ * @param keyword
+ * the particular keyword (call rules.getKeywords() to get the valid ones)
+ * @param offset
+ * the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
+ * checking against the keyword values.
+ * @param explicits
+ * a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+ * @param uniqueValue
+ * If non null, set to the unique value.
+ * @return the KeywordStatus
+ * @internal
+ * @provisional This API might change or be removed in a future release.
+ */
+ public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
+ Output<Double> uniqueValue, SampleType sampleType) {
+ if (uniqueValue != null) {
+ uniqueValue.value = null;
+ }
+
+ if (!keywords.contains(keyword)) {
+ return KeywordStatus.INVALID;
+ }
+
+ if (!isLimited(keyword, sampleType)) {
+ return KeywordStatus.UNBOUNDED;
+ }
+
+ Collection<Double> values = getSamples(keyword, sampleType);
+
+ int originalSize = values.size();
+
+ if (explicits == null) {
+ explicits = Collections.emptySet();
+ }
+
+ // Quick check on whether there are multiple elements
+
+ if (originalSize > explicits.size()) {
+ if (originalSize == 1) {
+ if (uniqueValue != null) {
+ uniqueValue.value = values.iterator().next();
+ }
+ return KeywordStatus.UNIQUE;
+ }
+ return KeywordStatus.BOUNDED;
+ }
+
+ // Compute if the quick test is insufficient.
+
+ HashSet<Double> subtractedSet = new HashSet<Double>(values);
+ for (Double explicit : explicits) {
+ subtractedSet.remove(explicit - offset);
+ }
+ if (subtractedSet.size() == 0) {
+ return KeywordStatus.SUPPRESSED;
+ }
+
+ if (uniqueValue != null && subtractedSet.size() == 1) {
+ uniqueValue.value = subtractedSet.iterator().next();
+ }
+
+ return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED;
+ }
+
+ /**
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ public String getRules(String keyword) {
+ return rules.getRules(keyword);
+ }
+
+ private void writeObject(
+ ObjectOutputStream out)
+ throws IOException {
+ throw new NotSerializableException();
+ }
+
+ private void readObject(ObjectInputStream in
+ ) throws IOException, ClassNotFoundException {
+ throw new NotSerializableException();
+ }
+
+ private Object writeReplace() throws ObjectStreamException {
+ return new PluralRulesSerialProxy(toString());
+ }
+
+ /**
+ * @internal
+ * @deprecated internal
+ */
+ public int compareTo(PluralRules other) {
+ return toString().compareTo(other.toString());
+ }
+
+ /**
+ * @internal
+ * @deprecated internal
+ */
+ public Boolean isLimited(String keyword) {
+ return rules.isLimited(keyword, SampleType.INTEGER);
+ }
+
+ /**
+ * @internal
+ * @deprecated internal
+ */
+ public boolean isLimited(String keyword, SampleType sampleType) {
+ return rules.isLimited(keyword, sampleType);
+ }
+
+ /**
+ * @internal
+ * @deprecated internal
+ */
+ public boolean computeLimited(String keyword, SampleType sampleType) {
+ return rules.computeLimited(keyword, sampleType);
+ }
+}
diff --git a/unicode-license.txt b/unicode-license.txt
new file mode 100644
index 0000000..c748a43
--- /dev/null
+++ b/unicode-license.txt
@@ -0,0 +1,50 @@
+UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
+
+ Unicode Data Files include all data files under the directories
+http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
+http://www.unicode.org/cldr/data/. Unicode Data Files do not include PDF
+online code charts under the directory http://www.unicode.org/Public/.
+Software includes any source code published in the Unicode Standard or under
+the directories http://www.unicode.org/Public/,
+http://www.unicode.org/reports/, and http://www.unicode.org/cldr/data/.
+
+ NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES
+("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND
+AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF
+YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA
+FILES OR SOFTWARE.
+
+ COPYRIGHT AND PERMISSION NOTICE
+
+ Copyright © 1991-2012 Unicode, Inc. All rights reserved. Distributed under
+the Terms of Use in http://www.unicode.org/copyright.html.
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Unicode data files and any associated documentation (the "Data
+Files") or Unicode software and any associated documentation (the "Software")
+to deal in the Data Files or Software without restriction, including without
+limitation the rights to use, copy, modify, merge, publish, distribute, and/or
+sell copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that (a) the above
+copyright notice(s) and this permission notice appear with all copies of the
+Data Files or Software, (b) both the above copyright notice(s) and this
+permission notice appear in associated documentation, and (c) there is clear
+notice in each modified Data File or in the Software as well as in the
+documentation associated with the Data File(s) or Software that the data or
+software has been modified.
+
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD
+PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN
+THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE
+DATA FILES OR SOFTWARE.
+
+ Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written authorization
+of the copyright holder. \ No newline at end of file