blob: 50a812f87e3f8cd1cbb59a20b02fe3de589c967b [file] [log] [blame]
// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.UnicodeSet;
/** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier {
// These are the default currency spacing UnicodeSets in CLDR.
// Pre-compute them for performance.
// The unit test testCurrencySpacingPatternStability() will start failing if these change in CLDR.
private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
// Constants for better readability. Types are for compiler checking.
static final byte PREFIX = 0;
static final byte SUFFIX = 1;
static final short IN_CURRENCY = 0;
static final short IN_NUMBER = 1;
private final UnicodeSet afterPrefixUnicodeSet;
private final String afterPrefixInsert;
private final UnicodeSet beforeSuffixUnicodeSet;
private final String beforeSuffixInsert;
/** Safe code path */
public CurrencySpacingEnabledModifier(
FormattedStringBuilder prefix,
FormattedStringBuilder suffix,
boolean overwrite,
boolean strong,
DecimalFormatSymbols symbols) {
super(prefix, suffix, overwrite, strong);
// Check for currency spacing. Do not build the UnicodeSets unless there is
// a currency code point at a boundary.
if (prefix.length() > 0 && prefix.fieldAt(prefix.length() - 1) == NumberFormat.Field.CURRENCY) {
int prefixCp = prefix.getLastCodePoint();
UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
if (prefixUnicodeSet.contains(prefixCp)) {
afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
afterPrefixUnicodeSet.freeze(); // no-op if set is already frozen
afterPrefixInsert = getInsertString(symbols, PREFIX);
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
} else {
afterPrefixUnicodeSet = null;
afterPrefixInsert = null;
}
if (suffix.length() > 0 && suffix.fieldAt(0) == NumberFormat.Field.CURRENCY) {
int suffixCp = suffix.getFirstCodePoint();
UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX);
if (suffixUnicodeSet.contains(suffixCp)) {
beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX);
beforeSuffixUnicodeSet.freeze(); // no-op if set is already frozen
beforeSuffixInsert = getInsertString(symbols, SUFFIX);
} else {
beforeSuffixUnicodeSet = null;
beforeSuffixInsert = null;
}
} else {
beforeSuffixUnicodeSet = null;
beforeSuffixInsert = null;
}
}
/** Safe code path */
@Override
public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) {
// Currency spacing logic
int length = 0;
if (rightIndex - leftIndex > 0
&& afterPrefixUnicodeSet != null
&& afterPrefixUnicodeSet.contains(output.codePointAt(leftIndex))) {
// TODO: Should we use the CURRENCY field here?
length += output.insert(leftIndex, afterPrefixInsert, null);
}
if (rightIndex - leftIndex > 0
&& beforeSuffixUnicodeSet != null
&& beforeSuffixUnicodeSet.contains(output.codePointBefore(rightIndex))) {
// TODO: Should we use the CURRENCY field here?
length += output.insert(rightIndex + length, beforeSuffixInsert, null);
}
// Call super for the remaining logic
length += super.apply(output, leftIndex, rightIndex + length);
return length;
}
/** Unsafe code path */
public static int applyCurrencySpacing(
FormattedStringBuilder output,
int prefixStart,
int prefixLen,
int suffixStart,
int suffixLen,
DecimalFormatSymbols symbols) {
int length = 0;
boolean hasPrefix = (prefixLen > 0);
boolean hasSuffix = (suffixLen > 0);
boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string
if (hasPrefix && hasNumber) {
length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols);
}
if (hasSuffix && hasNumber) {
length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols);
}
return length;
}
/** Unsafe code path */
private static int applyCurrencySpacingAffix(
FormattedStringBuilder output,
int index,
byte affix,
DecimalFormatSymbols symbols) {
// NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
// This works even if the last code point in the prefix is 2 code units because the
// field value gets populated to both indices in the field array.
Object affixField = (affix == PREFIX) ? output.fieldAt(index - 1)
: output.fieldAt(index);
if (affixField != NumberFormat.Field.CURRENCY) {
return 0;
}
int affixCp = (affix == PREFIX) ? output.codePointBefore(index) : output.codePointAt(index);
UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix);
if (!affixUniset.contains(affixCp)) {
return 0;
}
int numberCp = (affix == PREFIX) ? output.codePointAt(index) : output.codePointBefore(index);
UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix);
if (!numberUniset.contains(numberCp)) {
return 0;
}
String spacingString = getInsertString(symbols, affix);
// NOTE: This next line *inserts* the spacing string, triggering an arraycopy.
// It would be more efficient if this could be done before affixes were attached,
// so that it could be prepended/appended instead of inserted.
// However, the build code path is more efficient, and this is the most natural
// place to put currency spacing in the non-build code path.
// TODO: Should we use the CURRENCY field here?
return output.insert(index, spacingString, null);
}
private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) {
String pattern = symbols
.getPatternForCurrencySpacing(
position == IN_CURRENCY ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH
: DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH,
affix == SUFFIX);
if (pattern.equals("[:digit:]")) {
return UNISET_DIGIT;
} else if (pattern.equals("[:^S:]")) {
return UNISET_NOTS;
} else {
return new UnicodeSet(pattern);
}
}
private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
return symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT,
affix == SUFFIX);
}
}