]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-4_4_2-src/main/classes/core/src/com/ibm/icu/text/SelectFormat.java
go
[Dictionary.git] / jars / icu4j-4_4_2-src / main / classes / core / src / com / ibm / icu / text / SelectFormat.java
1 /*\r
2  *******************************************************************************\r
3  * Copyright (C) 2004-2010, International Business Machines Corporation and    *\r
4  * others. All Rights Reserved.                                                *\r
5  * Copyright (C) 2009 , Yahoo! Inc.                                            *\r
6  *******************************************************************************\r
7  */\r
8 package com.ibm.icu.text;\r
9 \r
10 import java.io.IOException;\r
11 import java.io.ObjectInputStream;\r
12 import java.text.FieldPosition;\r
13 import java.text.Format;\r
14 import java.text.ParsePosition;\r
15 import java.util.HashMap;\r
16 import java.util.Map;\r
17 \r
18 /**\r
19  * <p><code>SelectFormat</code> supports the creation of  internationalized\r
20  * messages by selecting phrases based on keywords. The pattern  specifies\r
21  * how to map keywords to phrases and provides a default phrase. The\r
22  * object provided to the format method is a string that's matched\r
23  * against the keywords. If there is a match, the corresponding phrase\r
24  * is selected; otherwise, the default phrase is used.</p>\r
25  *\r
26  * <h4>Using <code>SelectFormat</code> for Gender Agreement</h4>\r
27  *\r
28  * <p>The main use case for the select format is gender based  inflection.\r
29  * When names or nouns are inserted into sentences, their gender can  affect pronouns,\r
30  * verb forms, articles, and adjectives. Special care needs to be\r
31  * taken for the case where the gender cannot be determined.\r
32  * The impact varies between languages:</p>\r
33  *\r
34  * <ul>\r
35  * <li>English has three genders, and unknown gender is handled as a  special\r
36  * case. Names use the gender of the named person (if known), nouns  referring\r
37  * to people use natural gender, and inanimate objects are usually  neutral.\r
38  * The gender only affects pronouns: "he", "she", "it", "they".\r
39  *\r
40  * <li>German differs from English in that the gender of nouns is  rather\r
41  * arbitrary, even for nouns referring to people ("M&#u00E4;dchen", girl, is  neutral).\r
42  * The gender affects pronouns ("er", "sie", "es"), articles ("der",  "die",\r
43  * "das"), and adjective forms ("guter Mann", "gute Frau", "gutes  M&#u00E4;dchen").\r
44  *\r
45  * <li>French has only two genders; as in German the gender of nouns\r
46  * is rather arbitrary - for sun and moon, the genders\r
47  * are the opposite of those in German. The gender affects\r
48  * pronouns ("il", "elle"), articles ("le", "la"),\r
49  * adjective forms ("bon", "bonne"), and sometimes\r
50  * verb forms ("all&#u00E9;", "all&#u00E9e;").\r
51  *\r
52  * <li>Polish distinguishes five genders (or noun classes),\r
53  * human masculine, animate non-human masculine, inanimate masculine,\r
54  * feminine, and neuter.\r
55  * </ul>\r
56  *\r
57  * <p>Some other languages have noun classes that are not related to  gender,\r
58  * but similar in grammatical use.\r
59  * Some African languages have around 20 noun classes.</p>\r
60  *\r
61  * <p>To enable localizers to create sentence patterns that take their\r
62  * language's gender dependencies into consideration, software has to  provide\r
63  * information about the gender associated with a noun or name to\r
64  * <code>MessageFormat</code>.\r
65  * Two main cases can be distinguished:</p>\r
66  *\r
67  * <ul>\r
68  * <li>For people, natural gender information should be maintained  for each person.\r
69  * The keywords "male", "female", "mixed" (for groups of people)\r
70  * and "unknown" are used.\r
71  *\r
72  * <li>For nouns, grammatical gender information should be maintained  for\r
73  * each noun and per language, e.g., in resource bundles.\r
74  * The keywords "masculine", "feminine", and "neuter" are commonly  used,\r
75  * but some languages may require other keywords.\r
76  * </ul>\r
77  *\r
78  * <p>The resulting keyword is provided to <code>MessageFormat</code>  as a\r
79  * parameter separate from the name or noun it's associated with. For  example,\r
80  * to generate a message such as "Jean went to Paris", three separate  arguments\r
81  * would be provided: The name of the person as argument 0, the  gender of\r
82  * the person as argument 1, and the name of the city as argument 2.\r
83  * The sentence pattern for English, where the gender of the person has\r
84  * no impact on this simple sentence, would not refer to argument 1  at all:</p>\r
85  *\r
86  * <pre>{0} went to {2}.</pre>\r
87  *\r
88  * <p>The sentence pattern for French, where the gender of the person affects\r
89  * the form of the participle, uses a select format based on argument 1:</p>\r
90  *\r
91  * <pre>{0} est {1, select, female {all&#u00E9;e} other {all&#u00E9;}} &#u00E0; {2}.</pre>\r
92  *\r
93  * <p>Patterns can be nested, so that it's possible to handle  interactions of\r
94  * number and gender where necessary. For example, if the above  sentence should\r
95  * allow for the names of several people to be inserted, the  following sentence\r
96  * pattern can be used (with argument 0 the list of people's names,  \r
97  * argument 1 the number of people, argument 2 their combined gender, and  \r
98  * argument 3 the city name):</p>\r
99  *\r
100  * <pre>{0} {1, plural, \r
101  * one {est {2, select, female {all&#u00E9;e} other  {all&#u00E9;}}}\r
102  * other {sont {2, select, female {all&#u00E9;es} other {all&#u00E9;s}}}\r
103  * }&#u00E0; {3}.</pre>\r
104  *\r
105  * <h4>Patterns and Their Interpretation</h4>\r
106  *\r
107  * <p>The <code>SelectFormat</code> pattern text defines the phrase  output\r
108  * for each user-defined keyword.\r
109  * The pattern is a sequence of <code><i>keyword</i>{<i>phrase</i>}</code>\r
110  * clauses, separated by white space characters.\r
111  * Each clause assigns the phrase <code><i>phrase</i></code>\r
112  * to the user-defined <code><i>keyword</i></code>.</p>\r
113  *\r
114  * <p>Keywords must match the pattern [a-zA-Z][a-zA-Z0-9_-]*; keywords\r
115  * that don't match this pattern result in the error code\r
116  * <code>U_ILLEGAL_CHARACTER</code>.\r
117  * You always have to define a phrase for the default keyword\r
118  * <code>other</code>; this phrase is returned when the keyword  \r
119  * provided to\r
120  * the <code>format</code> method matches no other keyword.\r
121  * If a pattern does not provide a phrase for <code>other</code>, the  method\r
122  * it's provided to returns the error  <code>U_DEFAULT_KEYWORD_MISSING</code>.\r
123  * If a pattern provides more than one phrase for the same keyword, the\r
124  * error <code>U_DUPLICATE_KEYWORD</code> is returned.\r
125  * <br/>\r
126  * Spaces between <code><i>keyword</i></code> and\r
127  * <code>{<i>phrase</i>}</code>  will be ignored; spaces within\r
128  * <code>{<i>phrase</i>}</code> will be preserved.</p>\r
129  *\r
130  * <p>The phrase for a particular select case may contain other message\r
131  * format patterns. <code>SelectFormat</code> preserves these so that  you\r
132  * can use the strings produced by <code>SelectFormat</code> with other\r
133  * formatters. If you are using <code>SelectFormat</code> inside a\r
134  * <code>MessageFormat</code> pattern, <code>MessageFormat</code> will\r
135  * automatically evaluate the resulting format pattern.\r
136  * Thus, curly braces (<code>{</code>, <code>}</code>) are <i>only</i> allowed\r
137  * in phrases to define a nested format pattern.</p>\r
138  *\r
139  * <pre>Example:\r
140  * MessageFormat msgFmt = new MessageFormat("{0} est " +\r
141  *     "{1, select, female {all&#u00E9;e} other {all&#u00E9;}} &#u00E0; Paris.",\r
142  *     new ULocale("fr"));\r
143  * Object args[] = {"Kirti","female"};\r
144  * System.out.println(msgFmt.format(args));\r
145  * </pre>\r
146  * <p>\r
147  * Produces the output:<br/>\r
148  * <code>Kirti est all&#u00E9;e &#u00E0; Paris.</code>\r
149  * </p>\r
150  *\r
151  * @draft ICU 4.4\r
152  * @provisional This API might change or be removed in a future release.\r
153  */\r
154 \r
155 public class SelectFormat extends Format{\r
156     // Generated by serialver from JDK 1.5\r
157     private static final long serialVersionUID = 2993154333257524984L;\r
158 \r
159     /*\r
160      * The applied pattern string.\r
161      */\r
162     private String pattern = null;\r
163 \r
164     /*\r
165      * The format messages for each select case. It is a mapping:\r
166      *  <code>String</code>(select case keyword) --&gt; <code>String</code>\r
167      *  (message for this select case).\r
168      */\r
169     transient private Map<String, String> parsedValues = null;\r
170 \r
171     /**\r
172      * Common name for the default select form.  This name is returned\r
173      * for values to which no other form in the rule applies.  It \r
174      * can additionally be assigned rules of its own.\r
175      * @draft ICU 4.4\r
176      * @provisional This API might change or be removed in a future release.\r
177      */\r
178     private static final String KEYWORD_OTHER = "other";\r
179 \r
180     /*\r
181      * The types of character classifications \r
182      */\r
183     private enum CharacterClass {\r
184         T_START_KEYWORD, T_CONTINUE_KEYWORD, T_LEFT_BRACE,\r
185         T_RIGHT_BRACE, T_SPACE, T_OTHER\r
186     };\r
187 \r
188     /*\r
189      * The different states needed in state machine\r
190      * in applyPattern method. \r
191      */\r
192     private enum State {\r
193        START_STATE, KEYWORD_STATE,\r
194        PAST_KEYWORD_STATE, PHRASE_STATE      \r
195     };\r
196 \r
197     /**\r
198      * Creates a new <code>SelectFormat</code> for a given pattern string.\r
199      * @param  pattern the pattern for this <code>SelectFormat</code>.\r
200      * @draft ICU 4.4\r
201      * @provisional This API might change or be removed in a future release.\r
202      */\r
203     public SelectFormat(String pattern) {\r
204         init();\r
205         applyPattern(pattern);\r
206     }\r
207 \r
208     /*\r
209      * Initializes the <code>SelectFormat</code> object.\r
210      * Postcondition:<br/>\r
211      *   <code>parsedValues</code>: is <code>null</code><br/>\r
212      *   <code>pattern</code>:      is <code>null</code><br/>\r
213      */\r
214     private void init() {\r
215         parsedValues = null;\r
216         pattern = null;\r
217     }\r
218 \r
219     /**\r
220      * Classifies the characters \r
221      */\r
222     private boolean checkValidKeyword(String argKeyword) {\r
223         int len = argKeyword.length();\r
224         if (len < 1) {\r
225             return false;\r
226         };\r
227         if (classifyCharacter(argKeyword.charAt(0)) != CharacterClass.T_START_KEYWORD) {\r
228             return false;\r
229         };\r
230         for (int i = 1; i < len; i++) {\r
231             CharacterClass type = classifyCharacter(argKeyword.charAt(i));\r
232             if (type != CharacterClass.T_START_KEYWORD && \r
233                 type != CharacterClass.T_CONTINUE_KEYWORD) {\r
234                 return false;\r
235             };\r
236         };\r
237         return true;\r
238     }\r
239 \r
240     /**\r
241      * Classifies the characters.\r
242      */\r
243     private CharacterClass classifyCharacter(char ch) {\r
244         if ((ch >= 'A') && (ch <= 'Z')) {\r
245             return CharacterClass.T_START_KEYWORD;\r
246         }\r
247         if ((ch >= 'a') && (ch <= 'z')) {\r
248             return CharacterClass.T_START_KEYWORD;\r
249         }\r
250         if ((ch >= '0') && (ch <= '9')) {\r
251             return CharacterClass.T_CONTINUE_KEYWORD;\r
252         }\r
253         switch (ch) {\r
254             case '{':\r
255                 return CharacterClass.T_LEFT_BRACE;\r
256             case '}':\r
257                 return CharacterClass.T_RIGHT_BRACE;\r
258             case ' ':\r
259             case '\t':\r
260                 return CharacterClass.T_SPACE;\r
261             case '-':\r
262             case '_':\r
263                 return CharacterClass.T_CONTINUE_KEYWORD;\r
264             default :\r
265                 return CharacterClass.T_OTHER;\r
266         }\r
267     }\r
268 \r
269     /**\r
270      * Sets the pattern used by this select format.\r
271      * Patterns and their interpretation are specified in the class description.\r
272      *\r
273      * @param pattern the pattern for this select format.\r
274      * @throws IllegalArgumentException when the pattern is not a valid select format pattern.\r
275      * @draft ICU 4.4\r
276      * @provisional This API might change or be removed in a future release.\r
277      */\r
278     public void applyPattern(String pattern) {\r
279         parsedValues = null;\r
280         this.pattern = pattern;\r
281 \r
282         //Initialization\r
283         StringBuilder keyword = new StringBuilder();\r
284         StringBuilder phrase = new StringBuilder();\r
285         int braceCount = 0;\r
286 \r
287         parsedValues = new HashMap<String, String>();\r
288 \r
289         //Process the state machine\r
290         State state = State.START_STATE;\r
291         for (int i = 0; i < pattern.length(); i++ ){\r
292             //Get the character and check its type\r
293             char ch = pattern.charAt(i);\r
294             CharacterClass type = classifyCharacter(ch);\r
295 \r
296             //Process the state machine\r
297             switch (state) {\r
298                 //At the start of pattern\r
299                 case START_STATE:\r
300                     switch (type) {\r
301                         case T_SPACE:\r
302                             break ;\r
303                         case T_START_KEYWORD:\r
304                             state = State.KEYWORD_STATE;\r
305                             keyword.append(ch);\r
306                             break ;\r
307                         //If anything else is encountered, it's a syntax error\r
308                         default :\r
309                             parsingFailure("Pattern syntax error.");\r
310                 }//end of switch(type)\r
311                 break ;\r
312 \r
313                 //Handle the keyword state\r
314                 case KEYWORD_STATE:\r
315                     switch (type) {\r
316                         case T_SPACE:\r
317                             state = State.PAST_KEYWORD_STATE;\r
318                             break ;\r
319                         case T_START_KEYWORD:\r
320                         case T_CONTINUE_KEYWORD:\r
321                             keyword.append(ch);\r
322                             break ;\r
323                         case T_LEFT_BRACE:\r
324                             state = State.PHRASE_STATE;\r
325                         break ;\r
326                         //If anything else is encountered, it's a syntax error\r
327                         default :\r
328                             parsingFailure("Pattern syntax error.");\r
329                     }//end of switch(type)\r
330                     break ;\r
331 \r
332                 //Handle the pastkeyword state\r
333                 case PAST_KEYWORD_STATE:\r
334                     switch (type) {\r
335                         case T_SPACE:\r
336                             break ;\r
337                         case T_LEFT_BRACE:\r
338                             state = State.PHRASE_STATE;\r
339                             break ;\r
340                         //If anything else is encountered, it's a syntax error\r
341                         default :\r
342                             parsingFailure("Pattern syntax error.");\r
343                     }//end of switch(type)\r
344                         break ;\r
345 \r
346                //Handle the phrase state\r
347                case PHRASE_STATE:\r
348                     switch (type) {\r
349                         case T_LEFT_BRACE:\r
350                             braceCount++;\r
351                             phrase.append(ch);\r
352                             break ;\r
353                         case T_RIGHT_BRACE:\r
354                             //Matching keyword, phrase pair found\r
355                             if (braceCount == 0){\r
356                                 //Check validity of keyword\r
357                                 if (parsedValues.get(keyword.toString()) != null) {\r
358                                     parsingFailure("Duplicate keyword error.");\r
359                                 }\r
360                                 if (keyword.length() == 0) {\r
361                                     parsingFailure("Pattern syntax error.");\r
362                                 }\r
363 \r
364                                 //Store the keyword, phrase pair in hashTable\r
365                                 parsedValues.put( keyword.toString(), phrase.toString());\r
366 \r
367                                //Reinitialize\r
368                                 keyword.setLength(0);\r
369                                 phrase.setLength(0);\r
370                                 state = State.START_STATE;\r
371                             }\r
372 \r
373                             if (braceCount > 0){\r
374                                 braceCount-- ;\r
375                                 phrase.append(ch);\r
376                             }\r
377                             break ;\r
378                         default :\r
379                             phrase.append(ch);\r
380                     }//end of switch(type)\r
381                     break ;\r
382 \r
383                 //Handle the  default case of switch(state)\r
384                 default :\r
385                     parsingFailure("Pattern syntax error.");\r
386 \r
387             }//end of switch(state)\r
388         }\r
389 \r
390         //Check if the state machine is back to START_STATE\r
391         if ( state != State.START_STATE){\r
392             parsingFailure("Pattern syntax error.");\r
393         }\r
394 \r
395         //Check if "other" keyword is present \r
396         if ( !checkSufficientDefinition() ) {\r
397             parsingFailure("Pattern syntax error. " \r
398                     + "Value for case \"" + KEYWORD_OTHER\r
399                     + "\" was not defined. ");\r
400         }\r
401         return ;\r
402     }\r
403 \r
404     /**\r
405      * Returns the pattern for this <code>SelectFormat</code>\r
406      *\r
407      * @return the pattern string\r
408      * @draft ICU 4.4\r
409      * @provisional This API might change or be removed in a future release.\r
410      */\r
411     public String toPattern() {\r
412         return pattern;\r
413     }\r
414 \r
415     /**\r
416      * Selects the phrase for the given keyword.\r
417      *\r
418      * @param keyword a keyword for which the select message should be formatted.\r
419      * @return the string containing the formatted select message.\r
420      * @throws IllegalArgumentException when the given keyword is not available in the select format pattern\r
421      * @draft ICU 4.4\r
422      * @provisional This API might change or be removed in a future release.\r
423      */\r
424     public final String format(String keyword) {\r
425         //Check for the validity of the keyword\r
426         if( !checkValidKeyword(keyword) ){\r
427             throw new IllegalArgumentException("Invalid formatting argument.");\r
428         }\r
429 \r
430         // If no pattern was applied, throw an exception\r
431         if (parsedValues == null) {\r
432             throw new IllegalStateException("Invalid format error.");\r
433         }\r
434 \r
435         // Get appropriate format pattern.\r
436         String selectedPattern = parsedValues.get(keyword);\r
437         if (selectedPattern == null) { // Fallback to others.\r
438             selectedPattern = parsedValues.get(KEYWORD_OTHER);\r
439         }\r
440         return selectedPattern;\r
441     }\r
442 \r
443     /**\r
444      * Selects the phrase for the given keyword.\r
445      * and appends the formatted message to the given <code>StringBuffer</code>.\r
446      * @param keyword a keyword for which the select message should be formatted.\r
447      * @param toAppendTo the formatted message will be appended to this\r
448      *        <code>StringBuffer</code>.\r
449      * @param pos will be ignored by this method.\r
450      * @throws IllegalArgumentException when the given keyword is not available in the select format pattern\r
451      * @return the string buffer passed in as toAppendTo, with formatted text\r
452      *         appended.\r
453      * @draft ICU 4.4\r
454      * @provisional This API might change or be removed in a future release.\r
455      */\r
456     public StringBuffer format(Object keyword, StringBuffer toAppendTo,\r
457             FieldPosition pos) {\r
458         if (keyword instanceof String) {\r
459             toAppendTo.append(format( (String)keyword));\r
460         }else{\r
461             throw new IllegalArgumentException("'" + keyword + "' is not a String");\r
462         }\r
463         return toAppendTo;\r
464     }\r
465 \r
466     /**\r
467      * This method is not supported by <code>SelectFormat</code>.\r
468      * @param source the string to be parsed.\r
469      * @param pos defines the position where parsing is to begin,\r
470      * and upon return, the position where parsing left off.  If the position\r
471      * has not changed upon return, then parsing failed.\r
472      * @return nothing because this method is not supported.\r
473      * @throws UnsupportedOperationException thrown always.\r
474      * @draft ICU 4.4\r
475      * @provisional This API might change or be removed in a future release.\r
476      */\r
477     public Object parseObject(String source, ParsePosition pos) {\r
478         throw new UnsupportedOperationException();\r
479     }\r
480 \r
481     /*\r
482      * Checks if the applied pattern provided enough information,\r
483      * i.e., if the attribute <code>parsedValues</code> stores enough\r
484      * information for select formatting.\r
485      * Will be called at the end of pattern parsing.\r
486      */\r
487     private boolean checkSufficientDefinition() {\r
488         // Check that at least the default rule is defined.\r
489         return parsedValues.get(KEYWORD_OTHER) != null; \r
490     }\r
491 \r
492     /*\r
493      * Helper method that resets the <code>SelectFormat</code> object and throws\r
494      * an <code>IllegalArgumentException</code> with a given error text.\r
495      * @param errorText the error text of the exception message.\r
496      * @throws IllegalArgumentException will always be thrown by this method.\r
497      */\r
498     private void parsingFailure(String errorText) {\r
499         // Set SelectFormat to a valid state.\r
500         init();\r
501         throw new IllegalArgumentException(errorText);\r
502     }\r
503 \r
504     /**\r
505      * {@inheritDoc}\r
506      * @draft ICU 4.4\r
507      * @provisional This API might change or be removed in a future release.\r
508      */\r
509     public boolean equals(Object obj) {\r
510         if (!(obj instanceof SelectFormat)) {\r
511             return false;\r
512         }\r
513         SelectFormat sf = (SelectFormat) obj;\r
514         return pattern == null ? sf.pattern == null : pattern.equals(sf.pattern);\r
515     }\r
516 \r
517     /**\r
518      * {@inheritDoc}\r
519      * @draft ICU 4.4\r
520      * @provisional This API might change or be removed in a future release.\r
521      */\r
522     public int hashCode() {\r
523         if (pattern != null) {\r
524             return pattern.hashCode();\r
525         }\r
526         return 0;\r
527     }\r
528 \r
529     /**\r
530      * Returns a string representation of the object\r
531      * @return a text representation of the format object.\r
532      * The result string includes the class name and\r
533      * the pattern string returned by <code>toPattern()</code>.\r
534      * @draft ICU 4.4\r
535      * @provisional This API might change or be removed in a future release.\r
536      */\r
537     public String toString() {\r
538         StringBuilder buf = new StringBuilder();\r
539         buf.append("pattern='" + pattern + "'");\r
540         return buf.toString();\r
541     }\r
542 \r
543     private void readObject(ObjectInputStream in)\r
544         throws IOException, ClassNotFoundException {\r
545         in.defaultReadObject();\r
546         if (pattern != null) {\r
547             applyPattern(pattern);\r
548         }\r
549     }\r
550 }\r