442 lines
19 KiB
Java
442 lines
19 KiB
Java
![]() |
/* GENERATED SOURCE. DO NOT MODIFY. */
|
|||
|
// © 2018 and later: Unicode, Inc. and others.
|
|||
|
// License & terms of use: http://www.unicode.org/copyright.html
|
|||
|
package android.icu.number;
|
|||
|
|
|||
|
import java.util.MissingResourceException;
|
|||
|
|
|||
|
import android.icu.impl.FormattedStringBuilder;
|
|||
|
import android.icu.impl.FormattedValueStringBuilderImpl;
|
|||
|
import android.icu.impl.ICUData;
|
|||
|
import android.icu.impl.ICUResourceBundle;
|
|||
|
import android.icu.impl.PatternProps;
|
|||
|
import android.icu.impl.SimpleFormatterImpl;
|
|||
|
import android.icu.impl.StandardPlural;
|
|||
|
import android.icu.impl.UResource;
|
|||
|
import android.icu.impl.number.DecimalQuantity;
|
|||
|
import android.icu.impl.number.MacroProps;
|
|||
|
import android.icu.impl.number.MicroProps;
|
|||
|
import android.icu.impl.number.Modifier;
|
|||
|
import android.icu.impl.number.SimpleModifier;
|
|||
|
import android.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
|
|||
|
import android.icu.impl.number.range.RangeMacroProps;
|
|||
|
import android.icu.impl.number.range.StandardPluralRanges;
|
|||
|
import android.icu.number.NumberRangeFormatter.RangeCollapse;
|
|||
|
import android.icu.number.NumberRangeFormatter.RangeIdentityFallback;
|
|||
|
import android.icu.number.NumberRangeFormatter.RangeIdentityResult;
|
|||
|
import android.icu.text.NumberFormat;
|
|||
|
import android.icu.util.ULocale;
|
|||
|
import android.icu.util.UResourceBundle;
|
|||
|
|
|||
|
/**
|
|||
|
* Business logic behind NumberRangeFormatter.
|
|||
|
*/
|
|||
|
class NumberRangeFormatterImpl {
|
|||
|
|
|||
|
final NumberFormatterImpl formatterImpl1;
|
|||
|
final NumberFormatterImpl formatterImpl2;
|
|||
|
final boolean fSameFormatters;
|
|||
|
|
|||
|
final NumberRangeFormatter.RangeCollapse fCollapse;
|
|||
|
final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
|
|||
|
|
|||
|
// Should be final, but it is set in a helper function, not the constructor proper.
|
|||
|
// TODO: Clean up to make this field actually final.
|
|||
|
/* final */ String fRangePattern;
|
|||
|
final NumberFormatterImpl fApproximatelyFormatter;
|
|||
|
|
|||
|
final StandardPluralRanges fPluralRanges;
|
|||
|
|
|||
|
////////////////////
|
|||
|
|
|||
|
// Helper function for 2-dimensional switch statement
|
|||
|
int identity2d(RangeIdentityFallback a, RangeIdentityResult b) {
|
|||
|
return a.ordinal() | (b.ordinal() << 4);
|
|||
|
}
|
|||
|
|
|||
|
private static final class NumberRangeDataSink extends UResource.Sink {
|
|||
|
|
|||
|
String rangePattern;
|
|||
|
// Note: approximatelyPattern is unused since ICU 69.
|
|||
|
// String approximatelyPattern;
|
|||
|
|
|||
|
// For use with SimpleFormatterImpl
|
|||
|
StringBuilder sb;
|
|||
|
|
|||
|
NumberRangeDataSink(StringBuilder sb) {
|
|||
|
this.sb = sb;
|
|||
|
}
|
|||
|
|
|||
|
@Override
|
|||
|
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
|
|||
|
UResource.Table miscTable = value.getTable();
|
|||
|
for (int i = 0; miscTable.getKeyAndValue(i, key, value); ++i) {
|
|||
|
if (key.contentEquals("range") && !hasRangeData()) {
|
|||
|
String pattern = value.getString();
|
|||
|
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
|
|||
|
}
|
|||
|
/*
|
|||
|
// Note: approximatelyPattern is unused since ICU 69.
|
|||
|
if (key.contentEquals("approximately") && !hasApproxData()) {
|
|||
|
String pattern = value.getString();
|
|||
|
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}"
|
|||
|
}
|
|||
|
*/
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private boolean hasRangeData() {
|
|||
|
return rangePattern != null;
|
|||
|
}
|
|||
|
|
|||
|
/*
|
|||
|
// Note: approximatelyPattern is unused since ICU 69.
|
|||
|
private boolean hasApproxData() {
|
|||
|
return approximatelyPattern != null;
|
|||
|
}
|
|||
|
*/
|
|||
|
|
|||
|
public boolean isComplete() {
|
|||
|
return hasRangeData() /* && hasApproxData() */;
|
|||
|
}
|
|||
|
|
|||
|
public void fillInDefaults() {
|
|||
|
if (!hasRangeData()) {
|
|||
|
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2);
|
|||
|
}
|
|||
|
/*
|
|||
|
if (!hasApproxData()) {
|
|||
|
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
|
|||
|
}
|
|||
|
*/
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static void getNumberRangeData(
|
|||
|
ULocale locale,
|
|||
|
String nsName,
|
|||
|
NumberRangeFormatterImpl out) {
|
|||
|
StringBuilder sb = new StringBuilder();
|
|||
|
NumberRangeDataSink sink = new NumberRangeDataSink(sb);
|
|||
|
ICUResourceBundle resource;
|
|||
|
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
|
|||
|
sb.append("NumberElements/");
|
|||
|
sb.append(nsName);
|
|||
|
sb.append("/miscPatterns");
|
|||
|
String key = sb.toString();
|
|||
|
try {
|
|||
|
resource.getAllItemsWithFallback(key, sink);
|
|||
|
} catch (MissingResourceException e) {
|
|||
|
// ignore; fall back to latn
|
|||
|
}
|
|||
|
|
|||
|
// Fall back to latn if necessary
|
|||
|
if (!sink.isComplete()) {
|
|||
|
resource.getAllItemsWithFallback("NumberElements/latn/miscPatterns", sink);
|
|||
|
}
|
|||
|
|
|||
|
sink.fillInDefaults();
|
|||
|
|
|||
|
out.fRangePattern = sink.rangePattern;
|
|||
|
// out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
|
|||
|
}
|
|||
|
|
|||
|
////////////////////
|
|||
|
|
|||
|
public NumberRangeFormatterImpl(RangeMacroProps macros) {
|
|||
|
LocalizedNumberFormatter formatter1 = macros.formatter1 != null
|
|||
|
? macros.formatter1.locale(macros.loc)
|
|||
|
: NumberFormatter.withLocale(macros.loc);
|
|||
|
LocalizedNumberFormatter formatter2 = macros.formatter2 != null
|
|||
|
? macros.formatter2.locale(macros.loc)
|
|||
|
: NumberFormatter.withLocale(macros.loc);
|
|||
|
formatterImpl1 = new NumberFormatterImpl(formatter1.resolve());
|
|||
|
formatterImpl2 = new NumberFormatterImpl(formatter2.resolve());
|
|||
|
fSameFormatters = macros.sameFormatters != 0;
|
|||
|
fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO;
|
|||
|
fIdentityFallback = macros.identityFallback != null ? macros.identityFallback
|
|||
|
: NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY;
|
|||
|
|
|||
|
String nsName = formatterImpl1.getRawMicroProps().nsName;
|
|||
|
if (nsName == null || (!fSameFormatters && !nsName.equals(formatterImpl2.getRawMicroProps().nsName))) {
|
|||
|
throw new IllegalArgumentException("Both formatters must have same numbering system");
|
|||
|
}
|
|||
|
getNumberRangeData(macros.loc, nsName, this);
|
|||
|
|
|||
|
if (fSameFormatters && (
|
|||
|
fIdentityFallback == RangeIdentityFallback.APPROXIMATELY ||
|
|||
|
fIdentityFallback == RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)) {
|
|||
|
MacroProps approximatelyMacros = new MacroProps();
|
|||
|
approximatelyMacros.approximately = true;
|
|||
|
fApproximatelyFormatter = new NumberFormatterImpl(
|
|||
|
formatter1.macros(approximatelyMacros).resolve());
|
|||
|
} else {
|
|||
|
fApproximatelyFormatter = null;
|
|||
|
}
|
|||
|
|
|||
|
// TODO: Get locale from PluralRules instead?
|
|||
|
fPluralRanges = StandardPluralRanges.forLocale(macros.loc);
|
|||
|
}
|
|||
|
|
|||
|
public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
|
|||
|
FormattedStringBuilder string = new FormattedStringBuilder();
|
|||
|
MicroProps micros1 = formatterImpl1.preProcess(quantity1);
|
|||
|
MicroProps micros2;
|
|||
|
if (fSameFormatters) {
|
|||
|
micros2 = formatterImpl1.preProcess(quantity2);
|
|||
|
} else {
|
|||
|
micros2 = formatterImpl2.preProcess(quantity2);
|
|||
|
}
|
|||
|
|
|||
|
// If any of the affixes are different, an identity is not possible
|
|||
|
// and we must use formatRange().
|
|||
|
// TODO: Write this as MicroProps operator==() ?
|
|||
|
// TODO: Avoid the redundancy of these equality operations with the
|
|||
|
// ones in formatRange?
|
|||
|
if (!micros1.modInner.semanticallyEquivalent(micros2.modInner)
|
|||
|
|| !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle)
|
|||
|
|| !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) {
|
|||
|
formatRange(quantity1, quantity2, string, micros1, micros2);
|
|||
|
return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL);
|
|||
|
}
|
|||
|
|
|||
|
// Check for identity
|
|||
|
RangeIdentityResult identityResult;
|
|||
|
if (equalBeforeRounding) {
|
|||
|
identityResult = RangeIdentityResult.EQUAL_BEFORE_ROUNDING;
|
|||
|
} else if (quantity1.equals(quantity2)) {
|
|||
|
identityResult = RangeIdentityResult.EQUAL_AFTER_ROUNDING;
|
|||
|
} else {
|
|||
|
identityResult = RangeIdentityResult.NOT_EQUAL;
|
|||
|
}
|
|||
|
|
|||
|
// Java does not let us use a constexpr like C++;
|
|||
|
// we need to expand identity2d calls.
|
|||
|
switch (identity2d(fIdentityFallback, identityResult)) {
|
|||
|
case (3 | (2 << 4)): // RANGE, NOT_EQUAL
|
|||
|
case (3 | (1 << 4)): // RANGE, EQUAL_AFTER_ROUNDING
|
|||
|
case (3 | (0 << 4)): // RANGE, EQUAL_BEFORE_ROUNDING
|
|||
|
case (2 | (2 << 4)): // APPROXIMATELY, NOT_EQUAL
|
|||
|
case (1 | (2 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, NOT_EQUAL
|
|||
|
case (0 | (2 << 4)): // SINGLE_VALUE, NOT_EQUAL
|
|||
|
formatRange(quantity1, quantity2, string, micros1, micros2);
|
|||
|
break;
|
|||
|
|
|||
|
case (2 | (1 << 4)): // APPROXIMATELY, EQUAL_AFTER_ROUNDING
|
|||
|
case (2 | (0 << 4)): // APPROXIMATELY, EQUAL_BEFORE_ROUNDING
|
|||
|
case (1 | (1 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_AFTER_ROUNDING
|
|||
|
formatApproximately(quantity1, quantity2, string, micros1, micros2);
|
|||
|
break;
|
|||
|
|
|||
|
case (1 | (0 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
|
|||
|
case (0 | (1 << 4)): // SINGLE_VALUE, EQUAL_AFTER_ROUNDING
|
|||
|
case (0 | (0 << 4)): // SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
|
|||
|
formatSingleValue(quantity1, quantity2, string, micros1, micros2);
|
|||
|
break;
|
|||
|
|
|||
|
default:
|
|||
|
assert false;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
return new FormattedNumberRange(string, quantity1, quantity2, identityResult);
|
|||
|
}
|
|||
|
|
|||
|
private void formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string,
|
|||
|
MicroProps micros1, MicroProps micros2) {
|
|||
|
if (fSameFormatters) {
|
|||
|
int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
|
|||
|
NumberFormatterImpl.writeAffixes(micros1, string, 0, length);
|
|||
|
} else {
|
|||
|
formatRange(quantity1, quantity2, string, micros1, micros2);
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string,
|
|||
|
MicroProps micros1, MicroProps micros2) {
|
|||
|
if (fSameFormatters) {
|
|||
|
// Re-format using the approximately formatter:
|
|||
|
quantity1.resetExponent();
|
|||
|
MicroProps microsAppx = fApproximatelyFormatter.preProcess(quantity1);
|
|||
|
int length = NumberFormatterImpl.writeNumber(microsAppx, quantity1, string, 0);
|
|||
|
// HEURISTIC: Desired modifier order: inner, middle, approximately, outer.
|
|||
|
length += microsAppx.modInner.apply(string, 0, length);
|
|||
|
length += microsAppx.modMiddle.apply(string, 0, length);
|
|||
|
microsAppx.modOuter.apply(string, 0, length);
|
|||
|
} else {
|
|||
|
formatRange(quantity1, quantity2, string, micros1, micros2);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string,
|
|||
|
MicroProps micros1, MicroProps micros2) {
|
|||
|
// modInner is always notation (scientific); collapsable in ALL.
|
|||
|
// modOuter is always units; collapsable in ALL, AUTO, and UNIT.
|
|||
|
// modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT.
|
|||
|
// Never collapse an outer mod but not an inner mod.
|
|||
|
boolean collapseOuter, collapseMiddle, collapseInner;
|
|||
|
switch (fCollapse) {
|
|||
|
case ALL:
|
|||
|
case AUTO:
|
|||
|
case UNIT:
|
|||
|
{
|
|||
|
// OUTER MODIFIER
|
|||
|
collapseOuter = micros1.modOuter.semanticallyEquivalent(micros2.modOuter);
|
|||
|
|
|||
|
if (!collapseOuter) {
|
|||
|
// Never collapse inner mods if outer mods are not collapsable
|
|||
|
collapseMiddle = false;
|
|||
|
collapseInner = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
// MIDDLE MODIFIER
|
|||
|
collapseMiddle = micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle);
|
|||
|
|
|||
|
if (!collapseMiddle) {
|
|||
|
// Never collapse inner mods if outer mods are not collapsable
|
|||
|
collapseInner = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
// MIDDLE MODIFIER HEURISTICS
|
|||
|
// (could disable collapsing of the middle modifier)
|
|||
|
// The modifiers are equal by this point, so we can look at just one of them.
|
|||
|
Modifier mm = micros1.modMiddle;
|
|||
|
if (fCollapse == RangeCollapse.UNIT) {
|
|||
|
// Only collapse if the modifier is a unit.
|
|||
|
// TODO: Make a better way to check for a unit?
|
|||
|
// TODO: Handle case where the modifier has both notation and unit (compact currency)?
|
|||
|
if (!mm.containsField(NumberFormat.Field.CURRENCY) && !mm.containsField(NumberFormat.Field.PERCENT)) {
|
|||
|
collapseMiddle = false;
|
|||
|
}
|
|||
|
} else if (fCollapse == RangeCollapse.AUTO) {
|
|||
|
// Heuristic as of ICU 63: collapse only if the modifier is more than one code point.
|
|||
|
if (mm.getCodePointCount() <= 1) {
|
|||
|
collapseMiddle = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!collapseMiddle || fCollapse != RangeCollapse.ALL) {
|
|||
|
collapseInner = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
// INNER MODIFIER
|
|||
|
collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner);
|
|||
|
|
|||
|
// All done checking for collapsibility.
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
default:
|
|||
|
collapseOuter = false;
|
|||
|
collapseMiddle = false;
|
|||
|
collapseInner = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
// Java doesn't have macros, constexprs, or stack objects.
|
|||
|
// Use a helper object instead.
|
|||
|
PrefixInfixSuffixLengthHelper h = new PrefixInfixSuffixLengthHelper();
|
|||
|
|
|||
|
SimpleModifier.formatTwoArgPattern(fRangePattern, string, 0, h, null);
|
|||
|
assert h.lengthInfix > 0;
|
|||
|
|
|||
|
// SPACING HEURISTIC
|
|||
|
// Add spacing unless all modifiers are collapsed.
|
|||
|
// TODO: add API to control this?
|
|||
|
// TODO: Use a data-driven heuristic like currency spacing?
|
|||
|
// TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications)
|
|||
|
{
|
|||
|
boolean repeatInner = !collapseInner && micros1.modInner.getCodePointCount() > 0;
|
|||
|
boolean repeatMiddle = !collapseMiddle && micros1.modMiddle.getCodePointCount() > 0;
|
|||
|
boolean repeatOuter = !collapseOuter && micros1.modOuter.getCodePointCount() > 0;
|
|||
|
if (repeatInner || repeatMiddle || repeatOuter) {
|
|||
|
// Add spacing if there is not already spacing
|
|||
|
if (!PatternProps.isWhiteSpace(string.charAt(h.index1()))) {
|
|||
|
h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null);
|
|||
|
}
|
|||
|
if (!PatternProps.isWhiteSpace(string.charAt(h.index2() - 1))) {
|
|||
|
h.lengthInfix += string.insertCodePoint(h.index2(), '\u0020', null);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0());
|
|||
|
// ICU-21684: Write the second number to a temp string to avoid repeated insert operations
|
|||
|
FormattedStringBuilder tempString = new FormattedStringBuilder();
|
|||
|
NumberFormatterImpl.writeNumber(micros2, quantity2, tempString, 0);
|
|||
|
h.length2 += string.insert(h.index2(), tempString);
|
|||
|
|
|||
|
// TODO: Support padding?
|
|||
|
|
|||
|
if (collapseInner) {
|
|||
|
Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner);
|
|||
|
h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
|
|||
|
h.lengthPrefix += mod.getPrefixLength();
|
|||
|
h.lengthSuffix -= mod.getPrefixLength();
|
|||
|
} else {
|
|||
|
h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
|
|||
|
h.length2 += micros2.modInner.apply(string, h.index2(), h.index4());
|
|||
|
}
|
|||
|
|
|||
|
if (collapseMiddle) {
|
|||
|
Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle);
|
|||
|
h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
|
|||
|
h.lengthPrefix += mod.getPrefixLength();
|
|||
|
h.lengthSuffix -= mod.getPrefixLength();
|
|||
|
} else {
|
|||
|
h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
|
|||
|
h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index4());
|
|||
|
}
|
|||
|
|
|||
|
if (collapseOuter) {
|
|||
|
Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter);
|
|||
|
h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
|
|||
|
h.lengthPrefix += mod.getPrefixLength();
|
|||
|
h.lengthSuffix -= mod.getPrefixLength();
|
|||
|
} else {
|
|||
|
h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
|
|||
|
h.length2 += micros2.modOuter.apply(string, h.index2(), h.index4());
|
|||
|
}
|
|||
|
|
|||
|
// Now that all pieces are added, save the span info.
|
|||
|
FormattedValueStringBuilderImpl.applySpanRange(
|
|||
|
string,
|
|||
|
NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN,
|
|||
|
0,
|
|||
|
h.index0(),
|
|||
|
h.index1());
|
|||
|
FormattedValueStringBuilderImpl.applySpanRange(
|
|||
|
string,
|
|||
|
NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN,
|
|||
|
1,
|
|||
|
h.index2(),
|
|||
|
h.index3());
|
|||
|
}
|
|||
|
|
|||
|
Modifier resolveModifierPlurals(Modifier first, Modifier second) {
|
|||
|
Modifier.Parameters firstParameters = first.getParameters();
|
|||
|
if (firstParameters == null) {
|
|||
|
// No plural form; return a fallback (e.g., the first)
|
|||
|
return first;
|
|||
|
}
|
|||
|
|
|||
|
Modifier.Parameters secondParameters = second.getParameters();
|
|||
|
if (secondParameters == null) {
|
|||
|
// No plural form; return a fallback (e.g., the first)
|
|||
|
return first;
|
|||
|
}
|
|||
|
|
|||
|
// Get the required plural form from data
|
|||
|
StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural);
|
|||
|
|
|||
|
// Get and return the new Modifier
|
|||
|
Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural);
|
|||
|
assert mod != null;
|
|||
|
return mod;
|
|||
|
}
|
|||
|
}
|