Line data Source code
1 : // © 2016 and later: Unicode, Inc. and others.
2 : // License & terms of use: http://www.unicode.org/copyright.html
3 : /*
4 : *******************************************************************************
5 : * Copyright (C) 2009-2015, International Business Machines Corporation and
6 : * others. All Rights Reserved.
7 : *******************************************************************************
8 : *
9 : * File PLURFMT.CPP
10 : *******************************************************************************
11 : */
12 :
13 : #include "unicode/decimfmt.h"
14 : #include "unicode/messagepattern.h"
15 : #include "unicode/plurfmt.h"
16 : #include "unicode/plurrule.h"
17 : #include "unicode/utypes.h"
18 : #include "cmemory.h"
19 : #include "messageimpl.h"
20 : #include "nfrule.h"
21 : #include "plurrule_impl.h"
22 : #include "uassert.h"
23 : #include "uhash.h"
24 : #include "precision.h"
25 : #include "visibledigits.h"
26 :
27 : #if !UCONFIG_NO_FORMATTING
28 :
29 : U_NAMESPACE_BEGIN
30 :
31 : static const UChar OTHER_STRING[] = {
32 : 0x6F, 0x74, 0x68, 0x65, 0x72, 0 // "other"
33 : };
34 :
35 0 : UOBJECT_DEFINE_RTTI_IMPLEMENTATION(PluralFormat)
36 :
37 0 : PluralFormat::PluralFormat(UErrorCode& status)
38 : : locale(Locale::getDefault()),
39 : msgPattern(status),
40 : numberFormat(NULL),
41 0 : offset(0) {
42 0 : init(NULL, UPLURAL_TYPE_CARDINAL, status);
43 0 : }
44 :
45 0 : PluralFormat::PluralFormat(const Locale& loc, UErrorCode& status)
46 : : locale(loc),
47 : msgPattern(status),
48 : numberFormat(NULL),
49 0 : offset(0) {
50 0 : init(NULL, UPLURAL_TYPE_CARDINAL, status);
51 0 : }
52 :
53 0 : PluralFormat::PluralFormat(const PluralRules& rules, UErrorCode& status)
54 : : locale(Locale::getDefault()),
55 : msgPattern(status),
56 : numberFormat(NULL),
57 0 : offset(0) {
58 0 : init(&rules, UPLURAL_TYPE_COUNT, status);
59 0 : }
60 :
61 0 : PluralFormat::PluralFormat(const Locale& loc,
62 : const PluralRules& rules,
63 0 : UErrorCode& status)
64 : : locale(loc),
65 : msgPattern(status),
66 : numberFormat(NULL),
67 0 : offset(0) {
68 0 : init(&rules, UPLURAL_TYPE_COUNT, status);
69 0 : }
70 :
71 0 : PluralFormat::PluralFormat(const Locale& loc,
72 : UPluralType type,
73 0 : UErrorCode& status)
74 : : locale(loc),
75 : msgPattern(status),
76 : numberFormat(NULL),
77 0 : offset(0) {
78 0 : init(NULL, type, status);
79 0 : }
80 :
81 0 : PluralFormat::PluralFormat(const UnicodeString& pat,
82 0 : UErrorCode& status)
83 : : locale(Locale::getDefault()),
84 : msgPattern(status),
85 : numberFormat(NULL),
86 0 : offset(0) {
87 0 : init(NULL, UPLURAL_TYPE_CARDINAL, status);
88 0 : applyPattern(pat, status);
89 0 : }
90 :
91 0 : PluralFormat::PluralFormat(const Locale& loc,
92 : const UnicodeString& pat,
93 0 : UErrorCode& status)
94 : : locale(loc),
95 : msgPattern(status),
96 : numberFormat(NULL),
97 0 : offset(0) {
98 0 : init(NULL, UPLURAL_TYPE_CARDINAL, status);
99 0 : applyPattern(pat, status);
100 0 : }
101 :
102 0 : PluralFormat::PluralFormat(const PluralRules& rules,
103 : const UnicodeString& pat,
104 0 : UErrorCode& status)
105 : : locale(Locale::getDefault()),
106 : msgPattern(status),
107 : numberFormat(NULL),
108 0 : offset(0) {
109 0 : init(&rules, UPLURAL_TYPE_COUNT, status);
110 0 : applyPattern(pat, status);
111 0 : }
112 :
113 0 : PluralFormat::PluralFormat(const Locale& loc,
114 : const PluralRules& rules,
115 : const UnicodeString& pat,
116 0 : UErrorCode& status)
117 : : locale(loc),
118 : msgPattern(status),
119 : numberFormat(NULL),
120 0 : offset(0) {
121 0 : init(&rules, UPLURAL_TYPE_COUNT, status);
122 0 : applyPattern(pat, status);
123 0 : }
124 :
125 0 : PluralFormat::PluralFormat(const Locale& loc,
126 : UPluralType type,
127 : const UnicodeString& pat,
128 0 : UErrorCode& status)
129 : : locale(loc),
130 : msgPattern(status),
131 : numberFormat(NULL),
132 0 : offset(0) {
133 0 : init(NULL, type, status);
134 0 : applyPattern(pat, status);
135 0 : }
136 :
137 0 : PluralFormat::PluralFormat(const PluralFormat& other)
138 : : Format(other),
139 : locale(other.locale),
140 : msgPattern(other.msgPattern),
141 : numberFormat(NULL),
142 0 : offset(other.offset) {
143 0 : copyObjects(other);
144 0 : }
145 :
146 : void
147 0 : PluralFormat::copyObjects(const PluralFormat& other) {
148 0 : UErrorCode status = U_ZERO_ERROR;
149 0 : if (numberFormat != NULL) {
150 0 : delete numberFormat;
151 : }
152 0 : if (pluralRulesWrapper.pluralRules != NULL) {
153 0 : delete pluralRulesWrapper.pluralRules;
154 : }
155 :
156 0 : if (other.numberFormat == NULL) {
157 0 : numberFormat = NumberFormat::createInstance(locale, status);
158 : } else {
159 0 : numberFormat = (NumberFormat*)other.numberFormat->clone();
160 : }
161 0 : if (other.pluralRulesWrapper.pluralRules == NULL) {
162 0 : pluralRulesWrapper.pluralRules = PluralRules::forLocale(locale, status);
163 : } else {
164 0 : pluralRulesWrapper.pluralRules = other.pluralRulesWrapper.pluralRules->clone();
165 : }
166 0 : }
167 :
168 :
169 0 : PluralFormat::~PluralFormat() {
170 0 : delete numberFormat;
171 0 : }
172 :
173 : void
174 0 : PluralFormat::init(const PluralRules* rules, UPluralType type, UErrorCode& status) {
175 0 : if (U_FAILURE(status)) {
176 0 : return;
177 : }
178 :
179 0 : if (rules==NULL) {
180 0 : pluralRulesWrapper.pluralRules = PluralRules::forLocale(locale, type, status);
181 : } else {
182 0 : pluralRulesWrapper.pluralRules = rules->clone();
183 0 : if (pluralRulesWrapper.pluralRules == NULL) {
184 0 : status = U_MEMORY_ALLOCATION_ERROR;
185 0 : return;
186 : }
187 : }
188 :
189 0 : numberFormat= NumberFormat::createInstance(locale, status);
190 : }
191 :
192 : void
193 0 : PluralFormat::applyPattern(const UnicodeString& newPattern, UErrorCode& status) {
194 0 : msgPattern.parsePluralStyle(newPattern, NULL, status);
195 0 : if (U_FAILURE(status)) {
196 0 : msgPattern.clear();
197 0 : offset = 0;
198 0 : return;
199 : }
200 0 : offset = msgPattern.getPluralOffset(0);
201 : }
202 :
203 : UnicodeString&
204 0 : PluralFormat::format(const Formattable& obj,
205 : UnicodeString& appendTo,
206 : FieldPosition& pos,
207 : UErrorCode& status) const
208 : {
209 0 : if (U_FAILURE(status)) return appendTo;
210 :
211 0 : if (obj.isNumeric()) {
212 0 : return format(obj, obj.getDouble(), appendTo, pos, status);
213 : } else {
214 0 : status = U_ILLEGAL_ARGUMENT_ERROR;
215 0 : return appendTo;
216 : }
217 : }
218 :
219 : UnicodeString
220 0 : PluralFormat::format(int32_t number, UErrorCode& status) const {
221 0 : FieldPosition fpos(FieldPosition::DONT_CARE);
222 0 : UnicodeString result;
223 0 : return format(Formattable(number), number, result, fpos, status);
224 : }
225 :
226 : UnicodeString
227 0 : PluralFormat::format(double number, UErrorCode& status) const {
228 0 : FieldPosition fpos(FieldPosition::DONT_CARE);
229 0 : UnicodeString result;
230 0 : return format(Formattable(number), number, result, fpos, status);
231 : }
232 :
233 :
234 : UnicodeString&
235 0 : PluralFormat::format(int32_t number,
236 : UnicodeString& appendTo,
237 : FieldPosition& pos,
238 : UErrorCode& status) const {
239 0 : return format(Formattable(number), (double)number, appendTo, pos, status);
240 : }
241 :
242 : UnicodeString&
243 0 : PluralFormat::format(double number,
244 : UnicodeString& appendTo,
245 : FieldPosition& pos,
246 : UErrorCode& status) const {
247 0 : return format(Formattable(number), (double)number, appendTo, pos, status);
248 : }
249 :
250 : UnicodeString&
251 0 : PluralFormat::format(const Formattable& numberObject, double number,
252 : UnicodeString& appendTo,
253 : FieldPosition& pos,
254 : UErrorCode& status) const {
255 0 : if (U_FAILURE(status)) {
256 0 : return appendTo;
257 : }
258 0 : if (msgPattern.countParts() == 0) {
259 0 : return numberFormat->format(numberObject, appendTo, pos, status);
260 : }
261 : // Get the appropriate sub-message.
262 : // Select it based on the formatted number-offset.
263 0 : double numberMinusOffset = number - offset;
264 0 : UnicodeString numberString;
265 0 : FieldPosition ignorePos;
266 0 : FixedPrecision fp;
267 0 : VisibleDigitsWithExponent dec;
268 0 : fp.initVisibleDigitsWithExponent(numberMinusOffset, dec, status);
269 0 : if (U_FAILURE(status)) {
270 0 : return appendTo;
271 : }
272 0 : if (offset == 0) {
273 0 : DecimalFormat *decFmt = dynamic_cast<DecimalFormat *>(numberFormat);
274 0 : if(decFmt != NULL) {
275 : decFmt->initVisibleDigitsWithExponent(
276 0 : numberObject, dec, status);
277 0 : if (U_FAILURE(status)) {
278 0 : return appendTo;
279 : }
280 0 : decFmt->format(dec, numberString, ignorePos, status);
281 : } else {
282 0 : numberFormat->format(
283 0 : numberObject, numberString, ignorePos, status); // could be BigDecimal etc.
284 : }
285 : } else {
286 0 : DecimalFormat *decFmt = dynamic_cast<DecimalFormat *>(numberFormat);
287 0 : if(decFmt != NULL) {
288 : decFmt->initVisibleDigitsWithExponent(
289 0 : numberMinusOffset, dec, status);
290 0 : if (U_FAILURE(status)) {
291 0 : return appendTo;
292 : }
293 0 : decFmt->format(dec, numberString, ignorePos, status);
294 : } else {
295 0 : numberFormat->format(
296 0 : numberMinusOffset, numberString, ignorePos, status);
297 : }
298 : }
299 0 : int32_t partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, &dec, number, status);
300 0 : if (U_FAILURE(status)) { return appendTo; }
301 : // Replace syntactic # signs in the top level of this sub-message
302 : // (not in nested arguments) with the formatted number-offset.
303 0 : const UnicodeString& pattern = msgPattern.getPatternString();
304 0 : int32_t prevIndex = msgPattern.getPart(partIndex).getLimit();
305 : for (;;) {
306 0 : const MessagePattern::Part& part = msgPattern.getPart(++partIndex);
307 0 : const UMessagePatternPartType type = part.getType();
308 0 : int32_t index = part.getIndex();
309 0 : if (type == UMSGPAT_PART_TYPE_MSG_LIMIT) {
310 0 : return appendTo.append(pattern, prevIndex, index - prevIndex);
311 0 : } else if ((type == UMSGPAT_PART_TYPE_REPLACE_NUMBER) ||
312 0 : (type == UMSGPAT_PART_TYPE_SKIP_SYNTAX && MessageImpl::jdkAposMode(msgPattern))) {
313 0 : appendTo.append(pattern, prevIndex, index - prevIndex);
314 0 : if (type == UMSGPAT_PART_TYPE_REPLACE_NUMBER) {
315 0 : appendTo.append(numberString);
316 : }
317 0 : prevIndex = part.getLimit();
318 0 : } else if (type == UMSGPAT_PART_TYPE_ARG_START) {
319 0 : appendTo.append(pattern, prevIndex, index - prevIndex);
320 0 : prevIndex = index;
321 0 : partIndex = msgPattern.getLimitPartIndex(partIndex);
322 0 : index = msgPattern.getPart(partIndex).getLimit();
323 0 : MessageImpl::appendReducedApostrophes(pattern, prevIndex, index, appendTo);
324 0 : prevIndex = index;
325 : }
326 0 : }
327 : }
328 :
329 : UnicodeString&
330 0 : PluralFormat::toPattern(UnicodeString& appendTo) {
331 0 : if (0 == msgPattern.countParts()) {
332 0 : appendTo.setToBogus();
333 : } else {
334 0 : appendTo.append(msgPattern.getPatternString());
335 : }
336 0 : return appendTo;
337 : }
338 :
339 : void
340 0 : PluralFormat::setLocale(const Locale& loc, UErrorCode& status) {
341 0 : if (U_FAILURE(status)) {
342 0 : return;
343 : }
344 0 : locale = loc;
345 0 : msgPattern.clear();
346 0 : delete numberFormat;
347 0 : offset = 0;
348 0 : numberFormat = NULL;
349 0 : pluralRulesWrapper.reset();
350 0 : init(NULL, UPLURAL_TYPE_CARDINAL, status);
351 : }
352 :
353 : void
354 0 : PluralFormat::setNumberFormat(const NumberFormat* format, UErrorCode& status) {
355 0 : if (U_FAILURE(status)) {
356 0 : return;
357 : }
358 0 : NumberFormat* nf = (NumberFormat*)format->clone();
359 0 : if (nf != NULL) {
360 0 : delete numberFormat;
361 0 : numberFormat = nf;
362 : } else {
363 0 : status = U_MEMORY_ALLOCATION_ERROR;
364 : }
365 : }
366 :
367 : Format*
368 0 : PluralFormat::clone() const
369 : {
370 0 : return new PluralFormat(*this);
371 : }
372 :
373 :
374 : PluralFormat&
375 0 : PluralFormat::operator=(const PluralFormat& other) {
376 0 : if (this != &other) {
377 0 : locale = other.locale;
378 0 : msgPattern = other.msgPattern;
379 0 : offset = other.offset;
380 0 : copyObjects(other);
381 : }
382 :
383 0 : return *this;
384 : }
385 :
386 : UBool
387 0 : PluralFormat::operator==(const Format& other) const {
388 0 : if (this == &other) {
389 0 : return TRUE;
390 : }
391 0 : if (!Format::operator==(other)) {
392 0 : return FALSE;
393 : }
394 0 : const PluralFormat& o = (const PluralFormat&)other;
395 : return
396 0 : locale == o.locale &&
397 0 : msgPattern == o.msgPattern && // implies same offset
398 0 : (numberFormat == NULL) == (o.numberFormat == NULL) &&
399 0 : (numberFormat == NULL || *numberFormat == *o.numberFormat) &&
400 0 : (pluralRulesWrapper.pluralRules == NULL) == (o.pluralRulesWrapper.pluralRules == NULL) &&
401 0 : (pluralRulesWrapper.pluralRules == NULL ||
402 0 : *pluralRulesWrapper.pluralRules == *o.pluralRulesWrapper.pluralRules);
403 : }
404 :
405 : UBool
406 0 : PluralFormat::operator!=(const Format& other) const {
407 0 : return !operator==(other);
408 : }
409 :
410 : void
411 0 : PluralFormat::parseObject(const UnicodeString& /*source*/,
412 : Formattable& /*result*/,
413 : ParsePosition& pos) const
414 : {
415 : // Parsing not supported.
416 0 : pos.setErrorIndex(pos.getIndex());
417 0 : }
418 :
419 0 : int32_t PluralFormat::findSubMessage(const MessagePattern& pattern, int32_t partIndex,
420 : const PluralSelector& selector, void *context,
421 : double number, UErrorCode& ec) {
422 0 : if (U_FAILURE(ec)) {
423 0 : return 0;
424 : }
425 0 : int32_t count=pattern.countParts();
426 : double offset;
427 0 : const MessagePattern::Part* part=&pattern.getPart(partIndex);
428 0 : if (MessagePattern::Part::hasNumericValue(part->getType())) {
429 0 : offset=pattern.getNumericValue(*part);
430 0 : ++partIndex;
431 : } else {
432 0 : offset=0;
433 : }
434 : // The keyword is empty until we need to match against a non-explicit, not-"other" value.
435 : // Then we get the keyword from the selector.
436 : // (In other words, we never call the selector if we match against an explicit value,
437 : // or if the only non-explicit keyword is "other".)
438 0 : UnicodeString keyword;
439 0 : UnicodeString other(FALSE, OTHER_STRING, 5);
440 : // When we find a match, we set msgStart>0 and also set this boolean to true
441 : // to avoid matching the keyword again (duplicates are allowed)
442 : // while we continue to look for an explicit-value match.
443 0 : UBool haveKeywordMatch=FALSE;
444 : // msgStart is 0 until we find any appropriate sub-message.
445 : // We remember the first "other" sub-message if we have not seen any
446 : // appropriate sub-message before.
447 : // We remember the first matching-keyword sub-message if we have not seen
448 : // one of those before.
449 : // (The parser allows [does not check for] duplicate keywords.
450 : // We just have to make sure to take the first one.)
451 : // We avoid matching the keyword twice by also setting haveKeywordMatch=true
452 : // at the first keyword match.
453 : // We keep going until we find an explicit-value match or reach the end of the plural style.
454 0 : int32_t msgStart=0;
455 : // Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples
456 : // until ARG_LIMIT or end of plural-only pattern.
457 0 : do {
458 0 : part=&pattern.getPart(partIndex++);
459 0 : const UMessagePatternPartType type = part->getType();
460 0 : if(type==UMSGPAT_PART_TYPE_ARG_LIMIT) {
461 0 : break;
462 : }
463 0 : U_ASSERT (type==UMSGPAT_PART_TYPE_ARG_SELECTOR);
464 : // part is an ARG_SELECTOR followed by an optional explicit value, and then a message
465 0 : if(MessagePattern::Part::hasNumericValue(pattern.getPartType(partIndex))) {
466 : // explicit value like "=2"
467 0 : part=&pattern.getPart(partIndex++);
468 0 : if(number==pattern.getNumericValue(*part)) {
469 : // matches explicit value
470 0 : return partIndex;
471 : }
472 0 : } else if(!haveKeywordMatch) {
473 : // plural keyword like "few" or "other"
474 : // Compare "other" first and call the selector if this is not "other".
475 0 : if(pattern.partSubstringMatches(*part, other)) {
476 0 : if(msgStart==0) {
477 0 : msgStart=partIndex;
478 0 : if(0 == keyword.compare(other)) {
479 : // This is the first "other" sub-message,
480 : // and the selected keyword is also "other".
481 : // Do not match "other" again.
482 0 : haveKeywordMatch=TRUE;
483 : }
484 : }
485 : } else {
486 0 : if(keyword.isEmpty()) {
487 0 : keyword=selector.select(context, number-offset, ec);
488 0 : if(msgStart!=0 && (0 == keyword.compare(other))) {
489 : // We have already seen an "other" sub-message.
490 : // Do not match "other" again.
491 0 : haveKeywordMatch=TRUE;
492 : // Skip keyword matching but do getLimitPartIndex().
493 : }
494 : }
495 0 : if(!haveKeywordMatch && pattern.partSubstringMatches(*part, keyword)) {
496 : // keyword matches
497 0 : msgStart=partIndex;
498 : // Do not match this keyword again.
499 0 : haveKeywordMatch=TRUE;
500 : }
501 : }
502 : }
503 0 : partIndex=pattern.getLimitPartIndex(partIndex);
504 : } while(++partIndex<count);
505 0 : return msgStart;
506 : }
507 :
508 0 : void PluralFormat::parseType(const UnicodeString& source, const NFRule *rbnfLenientScanner, Formattable& result, FieldPosition& pos) const {
509 : // If no pattern was applied, return null.
510 0 : if (msgPattern.countParts() == 0) {
511 0 : pos.setBeginIndex(-1);
512 0 : pos.setEndIndex(-1);
513 0 : return;
514 : }
515 0 : int partIndex = 0;
516 : int currMatchIndex;
517 0 : int count=msgPattern.countParts();
518 0 : int startingAt = pos.getBeginIndex();
519 0 : if (startingAt < 0) {
520 0 : startingAt = 0;
521 : }
522 :
523 : // The keyword is null until we need to match against a non-explicit, not-"other" value.
524 : // Then we get the keyword from the selector.
525 : // (In other words, we never call the selector if we match against an explicit value,
526 : // or if the only non-explicit keyword is "other".)
527 0 : UnicodeString keyword;
528 0 : UnicodeString matchedWord;
529 0 : const UnicodeString& pattern = msgPattern.getPatternString();
530 0 : int matchedIndex = -1;
531 : // Iterate over (ARG_SELECTOR ARG_START message ARG_LIMIT) tuples
532 : // until the end of the plural-only pattern.
533 0 : while (partIndex < count) {
534 0 : const MessagePattern::Part* partSelector = &msgPattern.getPart(partIndex++);
535 0 : if (partSelector->getType() != UMSGPAT_PART_TYPE_ARG_SELECTOR) {
536 : // Bad format
537 0 : continue;
538 : }
539 :
540 0 : const MessagePattern::Part* partStart = &msgPattern.getPart(partIndex++);
541 0 : if (partStart->getType() != UMSGPAT_PART_TYPE_MSG_START) {
542 : // Bad format
543 0 : continue;
544 : }
545 :
546 0 : const MessagePattern::Part* partLimit = &msgPattern.getPart(partIndex++);
547 0 : if (partLimit->getType() != UMSGPAT_PART_TYPE_MSG_LIMIT) {
548 : // Bad format
549 0 : continue;
550 : }
551 :
552 0 : UnicodeString currArg = pattern.tempSubString(partStart->getLimit(), partLimit->getIndex() - partStart->getLimit());
553 0 : if (rbnfLenientScanner != NULL) {
554 : // If lenient parsing is turned ON, we've got some time consuming parsing ahead of us.
555 0 : int32_t length = -1;
556 0 : currMatchIndex = rbnfLenientScanner->findTextLenient(source, currArg, startingAt, &length);
557 : }
558 : else {
559 0 : currMatchIndex = source.indexOf(currArg, startingAt);
560 : }
561 0 : if (currMatchIndex >= 0 && currMatchIndex >= matchedIndex && currArg.length() > matchedWord.length()) {
562 0 : matchedIndex = currMatchIndex;
563 0 : matchedWord = currArg;
564 0 : keyword = pattern.tempSubString(partStart->getLimit(), partLimit->getIndex() - partStart->getLimit());
565 : }
566 : }
567 0 : if (matchedIndex >= 0) {
568 0 : pos.setBeginIndex(matchedIndex);
569 0 : pos.setEndIndex(matchedIndex + matchedWord.length());
570 0 : result.setString(keyword);
571 0 : return;
572 : }
573 :
574 : // Not found!
575 0 : pos.setBeginIndex(-1);
576 0 : pos.setEndIndex(-1);
577 : }
578 :
579 0 : PluralFormat::PluralSelector::~PluralSelector() {}
580 :
581 0 : PluralFormat::PluralSelectorAdapter::~PluralSelectorAdapter() {
582 0 : delete pluralRules;
583 0 : }
584 :
585 0 : UnicodeString PluralFormat::PluralSelectorAdapter::select(void *context, double number,
586 : UErrorCode& /*ec*/) const {
587 : (void)number; // unused except in the assertion
588 0 : VisibleDigitsWithExponent *dec=static_cast<VisibleDigitsWithExponent *>(context);
589 0 : return pluralRules->select(*dec);
590 : }
591 :
592 0 : void PluralFormat::PluralSelectorAdapter::reset() {
593 0 : delete pluralRules;
594 0 : pluralRules = NULL;
595 0 : }
596 :
597 :
598 : U_NAMESPACE_END
599 :
600 :
601 : #endif /* #if !UCONFIG_NO_FORMATTING */
602 :
603 : //eof
|