1
+ package com .mihnita .mf2 .messageformat ;
2
+
3
+ import static com .mihnita .mf2 .messageformat .helpers .ConstSelectors .CASE_EXACTLY_ONE ;
4
+ import static com .mihnita .mf2 .messageformat .helpers .ConstSelectors .CASE_EXACTLY_ZERO ;
5
+ import static com .mihnita .mf2 .messageformat .helpers .ConstSelectors .CASE_OTHER ;
6
+ import static org .junit .Assert .assertEquals ;
7
+
8
+ import java .util .ArrayList ;
9
+ import java .util .Collection ;
10
+ import java .util .HashMap ;
11
+ import java .util .List ;
12
+ import java .util .Locale ;
13
+ import java .util .Map ;
14
+
15
+ import org .junit .Test ;
16
+ import org .junit .runner .RunWith ;
17
+ import org .junit .runners .JUnit4 ;
18
+
19
+ import com .ibm .icu .text .ListFormatter ;
20
+ import com .ibm .icu .text .ListFormatter .Type ;
21
+ import com .ibm .icu .text .ListFormatter .Width ;
22
+ import com .ibm .icu .util .ULocale ;
23
+ import com .mihnita .mf2 .messageformat .datamodel .IPart ;
24
+ import com .mihnita .mf2 .messageformat .datamodel .ISelectorMessage .ISelectorVal ;
25
+ import com .mihnita .mf2 .messageformat .datamodel .functions .IPlaceholderFormatter ;
26
+ import com .mihnita .mf2 .messageformat .impl .Placeholder ;
27
+ import com .mihnita .mf2 .messageformat .impl .PlainText ;
28
+ import com .mihnita .mf2 .messageformat .impl .SelectorMessage ;
29
+ import com .mihnita .mf2 .messageformat .impl .SelectorMessage .SelectorArg ;
30
+ import com .mihnita .mf2 .messageformat .impl .SimpleMessage ;
31
+
32
+ @ RunWith (JUnit4 .class )
33
+ @ SuppressWarnings ("static-method" )
34
+ public class FancyListTest {
35
+
36
+ // List formatting with grammatical inflection on each list item #3
37
+ // https://github.com/unicode-org/message-format-wg/issues/3
38
+
39
+ @ Test
40
+ public void testGrammarCaseBlackBox () {
41
+ final String locale = "ro" ;
42
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("grammarBB" , new GrammarFormatter (locale ));
43
+
44
+ IPart [] parts = {
45
+ new PlainText ("Cartea " ),
46
+ new Placeholder ("owner" , "grammarBB" , Parameters .ph ().put ("case" , "genitive" ).build ())
47
+ };
48
+
49
+ SimpleMessage mf = new SimpleMessage ("" , locale , parts );
50
+
51
+ Map <String , Object > params = new HashMap <>();
52
+ params .put ("owner" , "Maria" );
53
+ assertEquals ("Cartea Mariei" , mf .format (params ));
54
+ params .put ("owner" , "Rodica" );
55
+ assertEquals ("Cartea Rodicăi" , mf .format (params ));
56
+ params .put ("owner" , "Ileana" );
57
+ assertEquals ("Cartea Ilenei" , mf .format (params ));
58
+ params .put ("owner" , "Petre" );
59
+ assertEquals ("Cartea lui Petre" , mf .format (params ));
60
+ }
61
+
62
+ @ Test
63
+ public void testSimpleList () {
64
+ final String locale = "ro" ;
65
+
66
+ String [] peopleNamesEmpty = {};
67
+ String [] peopleNames1Female = { "Maria" };
68
+ String [] peopleNames1Male = { "Petre" };
69
+ String [] peopleNames = { "Maria" , "Ileana" , "Petre" };
70
+
71
+ IPart [] partsExactly0 = {
72
+ new PlainText ("Nu am dat cadou nimanui" ),
73
+ };
74
+ IPart [] partsExactly1 = {
75
+ new PlainText ("I-am dat cadou " ),
76
+ new Placeholder ("people" , "listFormat" )
77
+ };
78
+ IPart [] partsOther = {
79
+ new PlainText ("Le-am dat cadou " ),
80
+ new Placeholder ("people" , "listFormat" )
81
+ };
82
+
83
+ final SelectorArg [] selectorArgs = { new SelectorArg ("listLen" , "plural" ) };
84
+ MsgMap messages = MsgMap .sel ()
85
+ .put (new ISelectorVal []{CASE_EXACTLY_ZERO }, new SimpleMessage ("" , locale , partsExactly0 ))
86
+ .put (new ISelectorVal []{CASE_EXACTLY_ONE }, new SimpleMessage ("" , locale , partsExactly1 ))
87
+ .put (new ISelectorVal []{CASE_OTHER }, new SimpleMessage ("" , locale , partsOther ));
88
+
89
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("listFormat" , new FormatList ());
90
+ final SelectorMessage mf = new SelectorMessage ("id" , locale , selectorArgs , messages .map ());
91
+
92
+ Map <String , Object > params = new HashMap <>();
93
+
94
+ params .put ("listLen" , peopleNamesEmpty .length );
95
+ params .put ("people" , peopleNamesEmpty );
96
+ assertEquals ("Nu am dat cadou nimanui" , mf .format (params ));
97
+
98
+ params .put ("listLen" , peopleNames1Female .length );
99
+ params .put ("people" , peopleNames1Female );
100
+ assertEquals ("I-am dat cadou Maria" , mf .format (params ));
101
+
102
+ params .put ("listLen" , peopleNames1Male .length );
103
+ params .put ("people" , peopleNames1Male );
104
+ assertEquals ("I-am dat cadou Petre" , mf .format (params ));
105
+
106
+ params .put ("listLen" , peopleNames .length );
107
+ params .put ("people" , peopleNames );
108
+ assertEquals ("Le-am dat cadou Maria, Ileana și Petre" , mf .format (params ));
109
+
110
+ partsOther [1 ] = new Placeholder ("people" , "listFormat" , Parameters .ph ().put ("listType" , "or" ).build ());
111
+ assertEquals ("Le-am dat cadou Maria, Ileana sau Petre" , mf .format (params ));
112
+
113
+ partsOther [1 ] = new Placeholder ("people" , "listFormat" , Parameters .ph ().put ("listWidth" , "narrow" ).build ());
114
+ assertEquals ("Le-am dat cadou Maria, Ileana, Petre" , mf .format (params ));
115
+ }
116
+
117
+ @ Test
118
+ public void testListWithItemProcess () {
119
+ final String locale = "ro" ;
120
+
121
+ String [] peopleNames = { "Maria" , "Ileana" , "Petre" };
122
+ IPart [] partsOther = {
123
+ new PlainText ("Le-am dat cadou " ),
124
+ new Placeholder ("people" , "listFormat" , Parameters .ph ()
125
+ .put ("listForEach" , "grammarBB" )
126
+ .put ("case" , "genitive" )
127
+ .build ())
128
+ };
129
+
130
+ SimpleMessage mf = new SimpleMessage ("" , locale , partsOther );
131
+
132
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("listFormat" , new FormatList ());
133
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("grammarBB" , new GrammarFormatter (locale ));
134
+
135
+ Map <String , Object > params = new HashMap <>();
136
+ params .put ("listLen" , peopleNames .length );
137
+ params .put ("people" , peopleNames );
138
+ assertEquals ("Le-am dat cadou Mariei, Ilenei și lui Petre" , mf .format (params ));
139
+ }
140
+
141
+ @ Test
142
+ public void testListWithItemMultiProcess () {
143
+ final String locale = "ro" ;
144
+
145
+ Person [] peopleNames = {
146
+ new Person ("Maria" , "Stanescu" ),
147
+ new Person ("Ileana" , "Zamfir" ),
148
+ new Person ("Petre" , "Belu" ),
149
+ };
150
+ IPart [] partsOther = {
151
+ new PlainText ("Le-am dat cadou " ),
152
+ new Placeholder ("people" , "listFormat" , Parameters .ph ()
153
+ .put ("listForEach" , "personName|grammarBB" )
154
+ .put ("case" , "genitive" )
155
+ .put ("personName" , "first" )
156
+ .build ())
157
+ };
158
+
159
+ SimpleMessage mf = new SimpleMessage ("" , locale , partsOther );
160
+
161
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("listFormat" , new FormatList ());
162
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("grammarBB" , new GrammarFormatter (locale ));
163
+ Placeholder .CUSTOM_FORMATTER_FUNC .put ("personName" , new GetPersonName ());
164
+
165
+ Map <String , Object > params = new HashMap <>();
166
+ params .put ("listLen" , peopleNames .length );
167
+ params .put ("people" , peopleNames );
168
+ assertEquals ("Le-am dat cadou Mariei, Ilenei și lui Petre" , mf .format (params ));
169
+
170
+ IPart [] partsOtherLastName = {
171
+ new PlainText ("Le-am dat cadou " ),
172
+ new Placeholder ("people" , "listFormat" , Parameters .ph ()
173
+ .put ("listForEach" , "personName|grammarBB" )
174
+ .put ("case" , "genitive" )
175
+ .put ("personName" , "last" ) // LAST instead of FIRST
176
+ .put ("listType" , "or" ) // OR instead of AND
177
+ .build ())
178
+ };
179
+ mf = new SimpleMessage ("" , locale , partsOtherLastName );
180
+ assertEquals ("Le-am dat cadou lui Stanescu, lui Zamfir sau lui Belu" , mf .format (params ));
181
+ }
182
+ }
183
+
184
+ class Person {
185
+ final String firstName ;
186
+ final String lastName ;
187
+ public Person (String firstName , String lastName ) {
188
+ this .firstName = firstName ;
189
+ this .lastName = lastName ;
190
+ }
191
+ }
192
+
193
+ class GetPersonName implements IPlaceholderFormatter {
194
+ @ Override
195
+ public String format (Object value , String locale , Map <String , String > options ) {
196
+ if (value instanceof Person ) {
197
+ Person person = (Person ) value ;
198
+ String field = (options == null ) ? "first" : options .get ("personName" );
199
+ if ("first" .equals (field )) {
200
+ return person .firstName ;
201
+ }
202
+ if ("last" .equals (field )) {
203
+ return person .lastName ;
204
+ }
205
+ return person .toString ();
206
+ }
207
+ return "{" + value + "}" ;
208
+ }
209
+ }
210
+
211
+ class Utils {
212
+ static IPlaceholderFormatter [] getFunctions (String functionNames ) {
213
+ String [] arrFunctionNames = functionNames .split ("\\ |" );
214
+ IPlaceholderFormatter [] result = new IPlaceholderFormatter [arrFunctionNames .length ];
215
+ for (int i = 0 ; i < arrFunctionNames .length ; i ++) {
216
+ result [i ] = Placeholder .CUSTOM_FORMATTER_FUNC .get (arrFunctionNames [i ]);
217
+ }
218
+ return result ;
219
+ }
220
+
221
+ static String applyFunctions (Object value , String locale , Map <String , String > options , IPlaceholderFormatter ... functions ) {
222
+ Object r = value ;
223
+ for (IPlaceholderFormatter function : functions ) {
224
+ r = function .format (r , locale , options );
225
+ }
226
+ return r .toString ();
227
+ }
228
+ }
229
+
230
+ class FormatList implements IPlaceholderFormatter {
231
+
232
+ Width stringToWidth (String val , Width fallback ) {
233
+ try {
234
+ return Width .valueOf (val .toUpperCase (Locale .US ));
235
+ } catch (IllegalArgumentException | NullPointerException expected ) {
236
+ return fallback ;
237
+ }
238
+ }
239
+
240
+ Type stringToType (String val , Type fallback ) {
241
+ try {
242
+ return Type .valueOf (val .toUpperCase (Locale .US ));
243
+ } catch (IllegalArgumentException | NullPointerException expected ) {
244
+ return fallback ;
245
+ }
246
+ }
247
+
248
+ static String safeMapGet (Map <String , String > options , String key ) {
249
+ return safeMapGet (options , key , "" );
250
+ }
251
+
252
+ static String safeMapGet (Map <String , String > options , String key , String def ) {
253
+ if (options == null )
254
+ return def ;
255
+ String result = options .get (key );
256
+ return result != null ? result : def ;
257
+ }
258
+
259
+ @ Override
260
+ public String format (Object value , String locale , Map <String , String > options ) {
261
+ Width width = stringToWidth (safeMapGet (options , "listWidth" ), Width .WIDE );
262
+ Type type = stringToType (safeMapGet (options , "listType" ), Type .AND );
263
+ String listForEach = safeMapGet (options , "listForEach" );
264
+
265
+ // IPlaceholderFormatter itemFunction = null;
266
+ IPlaceholderFormatter [] itemFunctions = null ;
267
+ if (!listForEach .isEmpty ()) {
268
+ itemFunctions = Utils .getFunctions (listForEach );
269
+ // itemFunction = Placeholder.CUSTOM_FORMATTER_FUNC.get(listForEach);
270
+ }
271
+
272
+ List <Object > toFormat = new ArrayList <>();
273
+ ListFormatter formatter = ListFormatter .getInstance (ULocale .forLanguageTag (locale ), type , width );
274
+ if (value instanceof Object []) {
275
+ for (Object v : (Object []) value ) {
276
+ if (itemFunctions != null ) {
277
+ // toFormat.add(itemFunction.format(v, locale, options));
278
+ toFormat .add (Utils .applyFunctions (v , locale , options , itemFunctions ));
279
+ } else {
280
+ toFormat .add (v );
281
+ }
282
+ }
283
+ }
284
+ if (value instanceof Collection <?>) {
285
+ for (Object v : (Collection <?>) value ) {
286
+ if (itemFunctions != null ) {
287
+ // toFormat.add(itemFunction.format(v, locale, options));
288
+ toFormat .add (Utils .applyFunctions (v , locale , options , itemFunctions ));
289
+ } else {
290
+ toFormat .add (v );
291
+ }
292
+ }
293
+ }
294
+ return formatter .format (toFormat );
295
+ }
296
+ }
297
+
298
+ class GrammarCasesBlackBox {
299
+ public GrammarCasesBlackBox (String localeId ) {
300
+ }
301
+
302
+ String getCase (String value , String grammarCase ) {
303
+ switch (grammarCase ) {
304
+ case "dative" : // intentional fallback
305
+ case "genitive" :
306
+ return getDativeAndGenitive (value );
307
+ // and so on, missing for now
308
+ default :
309
+ return value ;
310
+ }
311
+ }
312
+
313
+ private String getDativeAndGenitive (String value ) {
314
+ if (value .endsWith ("ana" ))
315
+ return value .substring (0 , value .length () - 3 ) + "nei" ;
316
+ if (value .endsWith ("ca" ))
317
+ return value .substring (0 , value .length () - 2 ) + "căi" ;
318
+ if (value .endsWith ("ga" ))
319
+ return value .substring (0 , value .length () - 2 ) + "găi" ;
320
+ if (value .endsWith ("a" ))
321
+ return value .substring (0 , value .length () - 1 ) + "ei" ;
322
+ return "lui " + value ;
323
+ }
324
+ }
325
+
326
+ class GrammarFormatter implements IPlaceholderFormatter {
327
+ private final GrammarCasesBlackBox grammarMagic ;
328
+
329
+ public GrammarFormatter (String locale ) {
330
+ grammarMagic = new GrammarCasesBlackBox (locale );
331
+ }
332
+
333
+ @ Override
334
+ public String format (Object value , String locale , Map <String , String > options ) {
335
+ String result = null ;
336
+ if (value instanceof CharSequence ) {
337
+ String gcase = options .get ("case" );
338
+ if (gcase == null )
339
+ gcase = "other" ;
340
+ result = grammarMagic .getCase (value .toString (), gcase );
341
+ }
342
+ return result == null ? "{" + value + "}" : result ;
343
+ }
344
+ }
0 commit comments