1 // © 2017 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
3
4 #include "unicode/utypes.h"
5
6 #if !UCONFIG_NO_FORMATTING
7
8 #include "unicode/dcfmtsym.h"
9
10 #include "cstr.h"
11 #include "numbertest.h"
12 #include "number_utils.h"
13 #include "number_skeletons.h"
14 #include "putilimp.h"
15
16 using namespace icu::number::impl;
17
18
runIndexedTest(int32_t index,UBool exec,const char * & name,char *)19 void NumberSkeletonTest::runIndexedTest(int32_t index, UBool exec, const char*& name, char*) {
20 if (exec) {
21 logln("TestSuite AffixUtilsTest: ");
22 }
23 TESTCASE_AUTO_BEGIN;
24 TESTCASE_AUTO(validTokens);
25 TESTCASE_AUTO(invalidTokens);
26 TESTCASE_AUTO(unknownTokens);
27 TESTCASE_AUTO(unexpectedTokens);
28 TESTCASE_AUTO(duplicateValues);
29 TESTCASE_AUTO(stemsRequiringOption);
30 TESTCASE_AUTO(defaultTokens);
31 TESTCASE_AUTO(flexibleSeparators);
32 TESTCASE_AUTO(wildcardCharacters);
33 TESTCASE_AUTO(perUnitInArabic);
34 TESTCASE_AUTO(perUnitToSkeleton);
35 TESTCASE_AUTO_END;
36 }
37
validTokens()38 void NumberSkeletonTest::validTokens() {
39 IcuTestErrorCode status(*this, "validTokens");
40
41 // This tests only if the tokens are valid, not their behavior.
42 // Most of these are from the design doc.
43 static const char16_t* cases[] = {
44 u"precision-integer",
45 u"precision-unlimited",
46 u"@@@##",
47 u"@@*",
48 u"@@+",
49 u".000##",
50 u".00*",
51 u".00+",
52 u".",
53 u".*",
54 u".+",
55 u".######",
56 u".00/@@*",
57 u".00/@@+",
58 u".00/@##",
59 u"precision-increment/3.14",
60 u"precision-currency-standard",
61 u"precision-integer rounding-mode-half-up",
62 u".00# rounding-mode-ceiling",
63 u".00/@@* rounding-mode-floor",
64 u".00/@@+ rounding-mode-floor",
65 u"scientific",
66 u"scientific/*ee",
67 u"scientific/+ee",
68 u"scientific/sign-always",
69 u"scientific/*ee/sign-always",
70 u"scientific/+ee/sign-always",
71 u"scientific/sign-always/*ee",
72 u"scientific/sign-always/+ee",
73 u"scientific/sign-except-zero",
74 u"engineering",
75 u"engineering/*eee",
76 u"engineering/+eee",
77 u"compact-short",
78 u"compact-long",
79 u"notation-simple",
80 u"percent",
81 u"permille",
82 u"measure-unit/length-meter",
83 u"measure-unit/area-square-meter",
84 u"measure-unit/energy-joule per-measure-unit/length-meter",
85 u"unit/square-meter-per-square-meter",
86 u"currency/XXX",
87 u"currency/ZZZ",
88 u"currency/usd",
89 u"group-off",
90 u"group-min2",
91 u"group-auto",
92 u"group-on-aligned",
93 u"group-thousands",
94 u"integer-width/00",
95 u"integer-width/#0",
96 u"integer-width/*00",
97 u"integer-width/+00",
98 u"sign-always",
99 u"sign-auto",
100 u"sign-never",
101 u"sign-accounting",
102 u"sign-accounting-always",
103 u"sign-except-zero",
104 u"sign-accounting-except-zero",
105 u"unit-width-narrow",
106 u"unit-width-short",
107 u"unit-width-iso-code",
108 u"unit-width-full-name",
109 u"unit-width-hidden",
110 u"decimal-auto",
111 u"decimal-always",
112 u"scale/5.2",
113 u"scale/-5.2",
114 u"scale/100",
115 u"scale/1E2",
116 u"scale/1",
117 u"latin",
118 u"numbering-system/arab",
119 u"numbering-system/latn",
120 u"precision-integer/@##",
121 u"precision-integer rounding-mode-ceiling",
122 u"precision-currency-cash rounding-mode-ceiling",
123 u"0",
124 u"00",
125 u"000",
126 u"E0",
127 u"E00",
128 u"E000",
129 u"EE0",
130 u"EE00",
131 u"EE+?0",
132 u"EE+?00",
133 u"EE+!0",
134 u"EE+!00",
135 };
136
137 for (auto& cas : cases) {
138 UnicodeString skeletonString(cas);
139 status.setScope(skeletonString);
140 UParseError perror;
141 NumberFormatter::forSkeleton(skeletonString, perror, status);
142 assertSuccess(CStr(skeletonString)(), status, true);
143 assertEquals(skeletonString, -1, perror.offset);
144 status.errIfFailureAndReset();
145 }
146 }
147
invalidTokens()148 void NumberSkeletonTest::invalidTokens() {
149 static const char16_t* cases[] = {
150 u".00x",
151 u".00##0",
152 u".##*",
153 u".00##*",
154 u".0#*",
155 u"@#*",
156 u".##+",
157 u".00##+",
158 u".0#+",
159 u"@#+",
160 u"@@x",
161 u"@@##0",
162 u".00/@",
163 u".00/@@",
164 u".00/@@x",
165 u".00/@@#",
166 u".00/@@#*",
167 u".00/floor/@@*", // wrong order
168 u".00/@@#+",
169 u".00/floor/@@+", // wrong order
170 u"precision-increment/français", // non-invariant characters for C++
171 u"scientific/ee",
172 u"precision-increment/xxx",
173 u"precision-increment/NaN",
174 u"precision-increment/0.1.2",
175 u"scale/xxx",
176 u"scale/NaN",
177 u"scale/0.1.2",
178 u"scale/français", // non-invariant characters for C++
179 u"currency/dummy",
180 u"currency/ççç", // three characters but not ASCII
181 u"measure-unit/foo",
182 u"integer-width/xxx",
183 u"integer-width/0*",
184 u"integer-width/*0#",
185 u"integer-width/*#",
186 u"integer-width/*#0",
187 u"integer-width/0+",
188 u"integer-width/+0#",
189 u"integer-width/+#",
190 u"integer-width/+#0",
191 u"scientific/foo",
192 u"E",
193 u"E1",
194 u"E+",
195 u"E+?",
196 u"E+!",
197 u"E+0",
198 u"EE",
199 u"EE+",
200 u"EEE",
201 u"EEE0",
202 u"001",
203 u"00*",
204 u"00+",
205 };
206
207 expectedErrorSkeleton(cases, UPRV_LENGTHOF(cases));
208 }
209
unknownTokens()210 void NumberSkeletonTest::unknownTokens() {
211 static const char16_t* cases[] = {
212 u"maesure-unit",
213 u"measure-unit/foo-bar",
214 u"numbering-system/dummy",
215 u"français",
216 u"measure-unit/français-français", // non-invariant characters for C++
217 u"numbering-system/français", // non-invariant characters for C++
218 u"currency-USD"};
219
220 expectedErrorSkeleton(cases, UPRV_LENGTHOF(cases));
221 }
222
unexpectedTokens()223 void NumberSkeletonTest::unexpectedTokens() {
224 static const char16_t* cases[] = {
225 u"group-thousands/foo",
226 u"precision-integer//@## group-off",
227 u"precision-integer//@## group-off",
228 u"precision-integer/ group-off",
229 u"precision-integer// group-off"};
230
231 expectedErrorSkeleton(cases, UPRV_LENGTHOF(cases));
232 }
233
duplicateValues()234 void NumberSkeletonTest::duplicateValues() {
235 static const char16_t* cases[] = {
236 u"precision-integer precision-integer",
237 u"precision-integer .00+",
238 u"precision-integer precision-unlimited",
239 u"precision-integer @@@",
240 u"scientific engineering",
241 u"engineering compact-long",
242 u"sign-auto sign-always"};
243
244 expectedErrorSkeleton(cases, UPRV_LENGTHOF(cases));
245 }
246
stemsRequiringOption()247 void NumberSkeletonTest::stemsRequiringOption() {
248 static const char16_t* stems[] = {
249 u"precision-increment",
250 u"measure-unit",
251 u"per-measure-unit",
252 u"currency",
253 u"integer-width",
254 u"numbering-system",
255 u"scale"};
256 static const char16_t* suffixes[] = {u"", u"/@##", u" scientific", u"/@## scientific"};
257
258 for (auto& stem : stems) {
259 for (auto& suffix : suffixes) {
260 UnicodeString skeletonString = UnicodeString(stem) + suffix;
261 UErrorCode status = U_ZERO_ERROR;
262 UParseError perror;
263 NumberFormatter::forSkeleton(skeletonString, perror, status);
264 assertEquals(skeletonString, U_NUMBER_SKELETON_SYNTAX_ERROR, status);
265
266 // Check the UParseError for integrity.
267 // If an option is present, the option is wrong; error offset is at the start of the option
268 // If an option is not present, the error offset is at the token separator (end of stem)
269 int32_t expectedOffset = u_strlen(stem) + ((suffix[0] == u'/') ? 1 : 0);
270 assertEquals(skeletonString, expectedOffset, perror.offset);
271 UnicodeString expectedPreContext = skeletonString.tempSubString(0, expectedOffset);
272 if (expectedPreContext.length() >= U_PARSE_CONTEXT_LEN - 1) {
273 expectedPreContext = expectedPreContext.tempSubString(expectedOffset - U_PARSE_CONTEXT_LEN + 1);
274 }
275 assertEquals(skeletonString, expectedPreContext, perror.preContext);
276 UnicodeString expectedPostContext = skeletonString.tempSubString(expectedOffset);
277 // None of the postContext strings in this test exceed U_PARSE_CONTEXT_LEN
278 assertEquals(skeletonString, expectedPostContext, perror.postContext);
279 }
280 }
281 }
282
defaultTokens()283 void NumberSkeletonTest::defaultTokens() {
284 IcuTestErrorCode status(*this, "defaultTokens");
285
286 static const char16_t* cases[] = {
287 u"notation-simple",
288 u"base-unit",
289 u"group-auto",
290 u"integer-width/+0",
291 u"sign-auto",
292 u"unit-width-short",
293 u"decimal-auto"};
294
295 for (auto& cas : cases) {
296 UnicodeString skeletonString(cas);
297 status.setScope(skeletonString);
298 UnicodeString normalized = NumberFormatter::forSkeleton(
299 skeletonString, status).toSkeleton(status);
300 // Skeleton should become empty when normalized
301 assertEquals(skeletonString, u"", normalized);
302 status.errIfFailureAndReset();
303 }
304 }
305
flexibleSeparators()306 void NumberSkeletonTest::flexibleSeparators() {
307 IcuTestErrorCode status(*this, "flexibleSeparators");
308
309 static struct TestCase {
310 const char16_t* skeleton;
311 const char16_t* expected;
312 } cases[] = {{u"precision-integer group-off", u"5142"},
313 {u"precision-integer group-off", u"5142"},
314 {u"precision-integer/@## group-off", u"5140"},
315 {u"precision-integer/@## group-off", u"5140"}};
316
317 for (auto& cas : cases) {
318 UnicodeString skeletonString(cas.skeleton);
319 UnicodeString expected(cas.expected);
320 status.setScope(skeletonString);
321 UnicodeString actual = NumberFormatter::forSkeleton(skeletonString, status).locale("en")
322 .formatDouble(5142.3, status)
323 .toString(status);
324 if (!status.errDataIfFailureAndReset()) {
325 assertEquals(skeletonString, expected, actual);
326 }
327 status.errIfFailureAndReset();
328 }
329 }
330
wildcardCharacters()331 void NumberSkeletonTest::wildcardCharacters() {
332 IcuTestErrorCode status(*this, "wildcardCharacters");
333
334 struct TestCase {
335 const char16_t* star;
336 const char16_t* plus;
337 } cases[] = {
338 { u".00*", u".00+" },
339 { u"@@*", u"@@+" },
340 { u".00/@@*", u".00/@@+" },
341 { u"scientific/*ee", u"scientific/+ee" },
342 { u"integer-width/*00", u"integer-width/+00" },
343 };
344
345 for (const auto& cas : cases) {
346 UnicodeString star(cas.star);
347 UnicodeString plus(cas.plus);
348 status.setScope(star);
349
350 UnicodeString normalized = NumberFormatter::forSkeleton(plus, status)
351 .toSkeleton(status);
352 assertEquals("Plus should normalize to star", star, normalized);
353 status.errIfFailureAndReset();
354 }
355 }
356
357 // In C++, there is no distinguishing between "invalid", "unknown", and "unexpected" tokens.
expectedErrorSkeleton(const char16_t ** cases,int32_t casesLen)358 void NumberSkeletonTest::expectedErrorSkeleton(const char16_t** cases, int32_t casesLen) {
359 for (int32_t i = 0; i < casesLen; i++) {
360 UnicodeString skeletonString(cases[i]);
361 UErrorCode status = U_ZERO_ERROR;
362 NumberFormatter::forSkeleton(skeletonString, status);
363 assertEquals(skeletonString, U_NUMBER_SKELETON_SYNTAX_ERROR, status);
364 }
365 }
366
perUnitInArabic()367 void NumberSkeletonTest::perUnitInArabic() {
368 IcuTestErrorCode status(*this, "perUnitInArabic");
369
370 struct TestCase {
371 const char16_t* type;
372 const char16_t* subtype;
373 } cases[] = {
374 {u"area", u"acre"},
375 {u"digital", u"bit"},
376 {u"digital", u"byte"},
377 {u"temperature", u"celsius"},
378 {u"length", u"centimeter"},
379 {u"duration", u"day"},
380 {u"angle", u"degree"},
381 {u"temperature", u"fahrenheit"},
382 {u"volume", u"fluid-ounce"},
383 {u"length", u"foot"},
384 {u"volume", u"gallon"},
385 {u"digital", u"gigabit"},
386 {u"digital", u"gigabyte"},
387 {u"mass", u"gram"},
388 {u"area", u"hectare"},
389 {u"duration", u"hour"},
390 {u"length", u"inch"},
391 {u"digital", u"kilobit"},
392 {u"digital", u"kilobyte"},
393 {u"mass", u"kilogram"},
394 {u"length", u"kilometer"},
395 {u"volume", u"liter"},
396 {u"digital", u"megabit"},
397 {u"digital", u"megabyte"},
398 {u"length", u"meter"},
399 {u"length", u"mile"},
400 {u"length", u"mile-scandinavian"},
401 {u"volume", u"milliliter"},
402 {u"length", u"millimeter"},
403 {u"duration", u"millisecond"},
404 {u"duration", u"minute"},
405 {u"duration", u"month"},
406 {u"mass", u"ounce"},
407 {u"concentr", u"percent"},
408 {u"digital", u"petabyte"},
409 {u"mass", u"pound"},
410 {u"duration", u"second"},
411 {u"mass", u"stone"},
412 {u"digital", u"terabit"},
413 {u"digital", u"terabyte"},
414 {u"duration", u"week"},
415 {u"length", u"yard"},
416 {u"duration", u"year"},
417 };
418
419 for (const auto& cas1 : cases) {
420 for (const auto& cas2 : cases) {
421 UnicodeString skeleton(u"measure-unit/");
422 skeleton += cas1.type;
423 skeleton += u"-";
424 skeleton += cas1.subtype;
425 skeleton += u" ";
426 skeleton += u"per-measure-unit/";
427 skeleton += cas2.type;
428 skeleton += u"-";
429 skeleton += cas2.subtype;
430
431 status.setScope(skeleton);
432 UnicodeString actual = NumberFormatter::forSkeleton(skeleton, status).locale("ar")
433 .formatDouble(5142.3, status)
434 .toString(status);
435 status.errIfFailureAndReset();
436 }
437 }
438 }
439
perUnitToSkeleton()440 void NumberSkeletonTest::perUnitToSkeleton() {
441 IcuTestErrorCode status(*this, "perUnitToSkeleton");
442 struct TestCase {
443 const char16_t* type;
444 const char16_t* subtype;
445 } cases[] = {
446 {u"area", u"acre"},
447 {u"concentr", u"percent"},
448 {u"concentr", u"permille"},
449 {u"concentr", u"permillion"},
450 {u"concentr", u"permyriad"},
451 {u"digital", u"bit"},
452 {u"length", u"yard"},
453 };
454
455 for (const auto& cas1 : cases) {
456 for (const auto& cas2 : cases) {
457 UnicodeString skeleton(u"measure-unit/");
458 skeleton += cas1.type;
459 skeleton += u"-";
460 skeleton += cas1.subtype;
461 skeleton += u" ";
462 skeleton += u"per-measure-unit/";
463 skeleton += cas2.type;
464 skeleton += u"-";
465 skeleton += cas2.subtype;
466
467 status.setScope(skeleton);
468 if (cas1.type != cas2.type && cas1.subtype != cas2.subtype) {
469 UnicodeString toSkeleton = NumberFormatter::forSkeleton(
470 skeleton, status).toSkeleton(status);
471 if (status.errIfFailureAndReset()) {
472 continue;
473 }
474 // Ensure both subtype are in the toSkeleton.
475 UnicodeString msg;
476 msg.append(toSkeleton)
477 .append(" should contain '")
478 .append(UnicodeString(cas1.subtype))
479 .append("' when constructed from ")
480 .append(skeleton);
481 assertTrue(msg, toSkeleton.indexOf(cas1.subtype) >= 0);
482
483 msg.remove();
484 msg.append(toSkeleton)
485 .append(" should contain '")
486 .append(UnicodeString(cas2.subtype))
487 .append("' when constructed from ")
488 .append(skeleton);
489 assertTrue(msg, toSkeleton.indexOf(cas2.subtype) >= 0);
490 }
491 }
492 }
493 }
494
495 #endif /* #if !UCONFIG_NO_FORMATTING */
496