ICU-11445 Bring per unit formatting in MeasureFormat out of tech preview.

X-SVN-Rev: 36885
This commit is contained in:
Travis Keep 2014-12-17 22:19:45 +00:00
parent b3897ebba9
commit 4cbb21d8b5
8 changed files with 625 additions and 240 deletions

1
.gitattributes vendored
View file

@ -268,6 +268,7 @@ icu4j/main/classes/core/.settings/edu.umd.cs.findbugs.core.prefs -text
icu4j/main/classes/core/.settings/org.eclipse.core.resources.prefs -text
icu4j/main/classes/core/.settings/org.eclipse.jdt.core.prefs -text
icu4j/main/classes/core/manifest.stub -text
icu4j/main/classes/core/src/com/ibm/icu/impl/Pair.java -text
icu4j/main/classes/currdata/.externalToolBuilders/copy-data-currdata.launch -text
icu4j/main/classes/currdata/.settings/org.eclipse.core.resources.prefs -text
icu4j/main/classes/currdata/.settings/org.eclipse.jdt.core.prefs -text

View file

@ -0,0 +1,53 @@
/*
*******************************************************************************
* Copyright (C) 2014, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.impl;
/**
* A pair of objects: first and second.
*
* @param <F> first object type
* @param <S> second object type
*/
public class Pair<F, S> {
public final F first;
public final S second;
protected Pair(F first, S second) {
this.first = first;
this.second = second;
}
/**
* Creates a pair object
* @param first must be non-null
* @param second must be non-null
* @return The pair object.
*/
public static <F, S> Pair<F, S> of(F first, S second) {
if (first == null || second == null) {
throw new IllegalArgumentException("Pair.of requires non null values.");
}
return new Pair<F, S>(first, second);
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Pair)) {
return false;
}
Pair<?, ?> rhs = (Pair<?, ?>) other;
return first.equals(rhs.first) && second.equals(rhs.second);
}
@Override
public int hashCode() {
return first.hashCode() * 37 + second.hashCode();
}
}

View file

@ -38,12 +38,15 @@ public class SimplePatternFormatter {
// [0] first offset; [1] first placeholderId; [2] second offset;
// [3] second placeholderId etc.
private final int[] placeholderIdsOrderedByOffset;
private final boolean firstPlaceholderReused;
private SimplePatternFormatter(String pattern, PlaceholdersBuilder builder) {
this.patternWithoutPlaceholders = pattern;
this.placeholderIdsOrderedByOffset =
builder.getPlaceholderIdsOrderedByOffset();
this.placeholderCount = builder.getPlaceholderCount();
this.firstPlaceholderReused = builder.getFirstPlaceholderReused();
}
/**
@ -51,7 +54,7 @@ public class SimplePatternFormatter {
* @param pattern The string.
* @return the new SimplePatternFormatter object.
*/
public static SimplePatternFormatter compile(CharSequence pattern) {
public static SimplePatternFormatter compile(String pattern) {
PlaceholdersBuilder placeholdersBuilder = new PlaceholdersBuilder();
PlaceholderIdBuilder idBuilder = new PlaceholderIdBuilder();
StringBuilder newPattern = new StringBuilder();
@ -121,89 +124,92 @@ public class SimplePatternFormatter {
return placeholderCount;
}
/**
* Returns true if this instance starts with placeholder with given id.
*/
public boolean startsWithPlaceholder(int id) {
if (placeholderIdsOrderedByOffset.length == 0) {
return false;
}
return (placeholderIdsOrderedByOffset[0] == 0 && placeholderIdsOrderedByOffset[1] == id);
}
/**
* Formats the given values.
*/
public String format(CharSequence... values) {
return format(new StringBuilder(), null, values).toString();
return formatAndAppend(new StringBuilder(), null, values).toString();
}
/**
* Formats the given values.
*
* @param appendTo the result appended here. Optimization: If the pattern this object
* represents starts with a placeholder AND appendTo references the value of that same
* placeholder (corresponding values parameter must also be a StringBuilder), then that
* placeholder value is not copied to appendTo (Its already there). If the value of the
* starting placeholder is very large, this optimization can offer huge savings.
* @param appendTo the result appended here.
* @param offsets position of first value in appendTo stored in offsets[0];
* second in offsets[1]; third in offsets[2] etc. An offset of -1 means that the
* corresponding value is not in appendTo. offsets.length and values.length may
* differ. If caller is not interested in offsets, caller may pass null here.
* @param values the values
* differ. If offsets.length < values.length then only the first offsets are written out;
* If offsets.length > values.length then the extra offsets get -1.
* If caller is not interested in offsets, caller may pass null here.
* @param values the placeholder values. A placeholder value may not be the same object as
* appendTo.
* @return appendTo
*/
public StringBuilder format(
public StringBuilder formatAndAppend(
StringBuilder appendTo, int[] offsets, CharSequence... values) {
if (values.length < placeholderCount) {
throw new IllegalArgumentException("Too few values.");
}
int offsetLen = offsets == null ? 0 : offsets.length;
for (int i = 0; i < offsetLen; i++) {
offsets[i] = -1;
PlaceholderValues placeholderValues = new PlaceholderValues(values);
if (placeholderValues.isAppendToInAnyIndexExcept(appendTo, -1)) {
throw new IllegalArgumentException("Parameter values cannot be the same as appendTo.");
}
if (placeholderIdsOrderedByOffset.length == 0) {
appendTo.append(patternWithoutPlaceholders);
return appendTo;
}
if (placeholderIdsOrderedByOffset[0] > 0 ||
appendTo != values[placeholderIdsOrderedByOffset[1]]) {
appendTo.append(
patternWithoutPlaceholders,
0,
placeholderIdsOrderedByOffset[0]);
setPlaceholderOffset(
placeholderIdsOrderedByOffset[1],
appendTo.length(),
offsets,
offsetLen);
appendTo.append(values[placeholderIdsOrderedByOffset[1]]);
} else {
setPlaceholderOffset(
placeholderIdsOrderedByOffset[1],
0,
offsets,
offsetLen);
}
for (int i = 2; i < placeholderIdsOrderedByOffset.length; i += 2) {
appendTo.append(
patternWithoutPlaceholders,
placeholderIdsOrderedByOffset[i - 2],
placeholderIdsOrderedByOffset[i]);
setPlaceholderOffset(
placeholderIdsOrderedByOffset[i + 1],
appendTo.length(),
offsets,
offsetLen);
appendTo.append(values[placeholderIdsOrderedByOffset[i + 1]]);
}
appendTo.append(
patternWithoutPlaceholders,
placeholderIdsOrderedByOffset[placeholderIdsOrderedByOffset.length - 2],
patternWithoutPlaceholders.length());
formatReturningOffsetLength(appendTo, offsets, placeholderValues);
return appendTo;
}
/**
* Formats the given values.
*
* @param result The result is stored here overwriting any previously stored value.
* @param offsets position of first value in result stored in offsets[0];
* second in offsets[1]; third in offsets[2] etc. An offset of -1 means that the
* corresponding value is not in result. offsets.length and values.length may
* differ. If offsets.length < values.length then only the first offsets are written out;
* If offsets.length > values.length then the extra offsets get -1.
* If caller is not interested in offsets, caller may pass null here.
* @param values the placeholder values. A placeholder value may be result itself in which case
* The previous value of result is used.
* @return result
*/
public StringBuilder formatAndReplace(
StringBuilder result, int[] offsets, CharSequence... values) {
if (values.length < placeholderCount) {
throw new IllegalArgumentException("Too few values.");
}
PlaceholderValues placeholderValues = new PlaceholderValues(values);
int placeholderAtStart = getUniquePlaceholderAtStart();
// If patterns starts with a placeholder and the value for that placeholder
// is result, then we can may be able optimize by just appending to result.
if (placeholderAtStart >= 0 && values[placeholderAtStart] == result) {
// If result is the value for other placeholders, call off optimization.
if (placeholderValues.isAppendToInAnyIndexExcept(result, placeholderAtStart)) {
placeholderValues.snapshotAppendTo(result);
result.setLength(0);
formatReturningOffsetLength(result, offsets, placeholderValues);
return result;
}
// Otherwise we can optimize
int offsetLength = formatReturningOffsetLength(result, offsets, placeholderValues);
// We have to make the offset for the placeholderAtStart placeholder be 0.
// Otherwise it would be the length of the previous value of result.
if (offsetLength > placeholderAtStart) {
offsets[placeholderAtStart] = 0;
}
return result;
}
if (placeholderValues.isAppendToInAnyIndexExcept(result, -1)) {
placeholderValues.snapshotAppendTo(result);
}
result.setLength(0);
formatReturningOffsetLength(result, offsets, placeholderValues);
return result;
}
/**
* Formats this object using values {0}, {1} etc. Note that this is
* not the same as the original pattern string used to build this object.
@ -214,7 +220,81 @@ public class SimplePatternFormatter {
for (int i = 0; i < values.length; i++) {
values[i] = String.format("{%d}", i);
}
return format(new StringBuilder(), null, values).toString();
return formatAndAppend(new StringBuilder(), null, values).toString();
}
/**
* Returns this pattern with none of the placeholders.
*/
public String getPatternWithNoPlaceholders() {
return patternWithoutPlaceholders;
}
/**
* Just like format, but uses placeholder values exactly as they are.
* A placeholder value that is the same object as appendTo is treated
* as the empty string. In addition, returns the length of the offsets
* array. Returns 0 if offsets is null.
*/
private int formatReturningOffsetLength(
StringBuilder appendTo,
int[] offsets,
PlaceholderValues values) {
int offsetLen = offsets == null ? 0 : offsets.length;
for (int i = 0; i < offsetLen; i++) {
offsets[i] = -1;
}
if (placeholderIdsOrderedByOffset.length == 0) {
appendTo.append(patternWithoutPlaceholders);
return offsetLen;
}
appendTo.append(
patternWithoutPlaceholders,
0,
placeholderIdsOrderedByOffset[0]);
setPlaceholderOffset(
placeholderIdsOrderedByOffset[1],
appendTo.length(),
offsets,
offsetLen);
CharSequence placeholderValue = values.get(placeholderIdsOrderedByOffset[1]);
if (placeholderValue != appendTo) {
appendTo.append(placeholderValue);
}
for (int i = 2; i < placeholderIdsOrderedByOffset.length; i += 2) {
appendTo.append(
patternWithoutPlaceholders,
placeholderIdsOrderedByOffset[i - 2],
placeholderIdsOrderedByOffset[i]);
setPlaceholderOffset(
placeholderIdsOrderedByOffset[i + 1],
appendTo.length(),
offsets,
offsetLen);
placeholderValue = values.get(placeholderIdsOrderedByOffset[i + 1]);
if (placeholderValue != appendTo) {
appendTo.append(placeholderValue);
}
}
appendTo.append(
patternWithoutPlaceholders,
placeholderIdsOrderedByOffset[placeholderIdsOrderedByOffset.length - 2],
patternWithoutPlaceholders.length());
return offsetLen;
}
/**
* Returns the placeholder at the beginning of this pattern (e.g 3 for placeholder {3}).
* Returns -1 if the beginning of pattern is text or if the placeholder at beginning
* of this pattern is used again elsewhere in pattern.
*/
private int getUniquePlaceholderAtStart() {
if (placeholderIdsOrderedByOffset.length == 0
|| firstPlaceholderReused || placeholderIdsOrderedByOffset[0] != 0) {
return -1;
}
return placeholderIdsOrderedByOffset[1];
}
private static void setPlaceholderOffset(
@ -262,6 +342,7 @@ public class SimplePatternFormatter {
private static class PlaceholdersBuilder {
private List<Integer> placeholderIdsOrderedByOffset = new ArrayList<Integer>();
private int placeholderCount = 0;
private boolean firstPlaceholderReused = false;
public void add(int placeholderId, int offset) {
placeholderIdsOrderedByOffset.add(offset);
@ -269,6 +350,12 @@ public class SimplePatternFormatter {
if (placeholderId >= placeholderCount) {
placeholderCount = placeholderId + 1;
}
int len = placeholderIdsOrderedByOffset.size();
if (len > 2
&& placeholderIdsOrderedByOffset.get(len - 1)
.equals(placeholderIdsOrderedByOffset.get(1))) {
firstPlaceholderReused = true;
}
}
public int getPlaceholderCount() {
@ -282,12 +369,55 @@ public class SimplePatternFormatter {
}
return result;
}
public boolean getFirstPlaceholderReused() {
return firstPlaceholderReused;
}
}
/**
* Returns this pattern with none of the placeholders.
* Represents placeholder values.
*/
public String getPatternWithNoPlaceholders() {
return patternWithoutPlaceholders;
private static class PlaceholderValues {
private final CharSequence[] values;
private CharSequence appendTo;
private String appendToCopy;
public PlaceholderValues(CharSequence ...values) {
this.values = values;
this.appendTo = null;
this.appendToCopy = null;
}
/**
* Returns true if appendTo value is at any index besides exceptIndex.
*/
public boolean isAppendToInAnyIndexExcept(CharSequence appendTo, int exceptIndex) {
for (int i = 0; i < values.length; ++i) {
if (i != exceptIndex && values[i] == appendTo) {
return true;
}
}
return false;
}
/**
* For each appendTo value, stores the snapshot of it in its place.
*/
public void snapshotAppendTo(CharSequence appendTo) {
this.appendTo = appendTo;
this.appendToCopy = appendTo.toString();
}
/**
* Return placeholder at given index.
*/
public CharSequence get(int index) {
if (appendTo == null || appendTo != values[index]) {
return values[index];
}
return appendToCopy;
}
}
}

View file

@ -270,10 +270,8 @@ final public class ListFormatter {
throw new IllegalArgumentException("Need {0} and {1} only in pattern " + pattern);
}
int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null;
StringBuilder nextBuilder =
pattern.startsWithPlaceholder(0) ? current : new StringBuilder();
current = pattern.format(
nextBuilder, offsets, current, next.toString());
pattern.formatAndReplace(
current, offsets, current, next.toString());
if (offsets != null) {
if (offsets[0] == -1 || offsets[1] == -1) {
throw new IllegalArgumentException(

View file

@ -498,30 +498,38 @@ public class MeasureFormat extends UFormat {
}
/**
* Like formatMeasures but formats with a per unit.
* Formats a single measure per unit.
*
* Will format to a string such as "5 kilometers, 300 meters per hour."
*
* @param appendTo the formatted string appended here.
* @param fieldPosition Identifies a field in the formatted text.
* @param perUnit for the example above would be MeasureUnit.HOUR.
* @param measures the measures to format.
* An example of such a formatted string is "3.5 meters per second."
*
* @param measure the measure object. In above example, 3.5 meters.
* @param perUnit the per unit. In above example, it is MeasureUnit.SECOND
* @param appendTo formatted string appended here.
* @param pos The field position.
* @return appendTo.
* @internal Technology preview
* @deprecated This API is ICU internal only.
* @draft ICU 55
* @provisional This API might change or be removed in a future release.
*/
@Deprecated
public StringBuilder formatMeasuresPer(
StringBuilder appendTo, FieldPosition fieldPosition, MeasureUnit perUnit, Measure... measures) {
public StringBuilder formatMeasurePerUnit(
Measure measure,
MeasureUnit perUnit,
StringBuilder appendTo,
FieldPosition pos) {
MeasureUnit resolvedUnit = MeasureUnit.resolveUnitPerUnit(
measure.getUnit(), perUnit);
if (resolvedUnit != null) {
Measure newMeasure = new Measure(measure.getNumber(), resolvedUnit);
return formatMeasure(newMeasure, numberFormat, appendTo, pos);
}
FieldPosition fpos = new FieldPosition(
fieldPosition.getFieldAttribute(), fieldPosition.getField());
int offset = withPerUnit(
formatMeasures(new StringBuilder(), fpos, measures),
pos.getFieldAttribute(), pos.getField());
int offset = withPerUnitAndAppend(
formatMeasure(measure, numberFormat, new StringBuilder(), fpos),
perUnit,
appendTo);
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset);
fieldPosition.setEndIndex(fpos.getEndIndex() + offset);
pos.setBeginIndex(fpos.getBeginIndex() + offset);
pos.setEndIndex(fpos.getEndIndex() + offset);
}
return appendTo;
}
@ -852,20 +860,21 @@ public class MeasureFormat extends UFormat {
return true;
}
private int withPerUnit(CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) {
private int withPerUnitAndAppend(
CharSequence formatted, MeasureUnit perUnit, StringBuilder appendTo) {
int[] offsets = new int[1];
Map<FormatWidth, SimplePatternFormatter> styleToPerUnitPattern =
unitToStyleToPerUnitPattern.get(perUnit);
SimplePatternFormatter perUnitPattern = styleToPerUnitPattern.get(formatWidth);
if (perUnitPattern != null) {
perUnitPattern.format(appendTo, offsets, formatted);
perUnitPattern.formatAndAppend(appendTo, offsets, formatted);
return offsets[0];
}
SimplePatternFormatter perPattern = styleToPerPattern.get(formatWidth);
Map<FormatWidth, QuantityFormatter> styleToCountToFormat = unitToStyleToCountToFormat.get(perUnit);
QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth);
String perUnitString = countToFormat.getByVariant("one").getPatternWithNoPlaceholders().trim();
perPattern.format(appendTo, offsets, formatted, perUnitString);
perPattern.formatAndAppend(appendTo, offsets, formatted, perUnitString);
return offsets[0];
}
@ -898,7 +907,7 @@ public class MeasureFormat extends UFormat {
QuantityFormatter countToFormat = styleToCountToFormat.get(formatWidth);
SimplePatternFormatter formatter = countToFormat.getByVariant(keyword);
int[] offsets = new int[1];
formatter.format(appendTo, offsets, formattedNumber);
formatter.formatAndAppend(appendTo, offsets, formattedNumber);
if (offsets[0] != -1) { // there is a number (may not happen with, say, Arabic dual)
// Fix field position
if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {

View file

@ -21,6 +21,7 @@ import java.util.MissingResourceException;
import java.util.Set;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.text.UnicodeSet;
/**
@ -196,6 +197,16 @@ public class MeasureUnit implements Serializable {
}
return MeasureUnit.addUnit(type, subType, factory);
}
/**
* For ICU use only.
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public static MeasureUnit resolveUnitPerUnit(MeasureUnit unit, MeasureUnit perUnit) {
return unitPerUnitToSingleUnit.get(Pair.of(unit, perUnit));
}
static final UnicodeSet ASCII = new UnicodeSet('a', 'z').freeze();
static final UnicodeSet ASCII_HYPHEN = new UnicodeSet('-', '-', 'a', 'z').freeze();
@ -1154,6 +1165,18 @@ public class MeasureUnit implements Serializable {
*/
public static final MeasureUnit TEASPOON = MeasureUnit.internalGetInstance("volume", "teaspoon");
private static HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>unitPerUnitToSingleUnit =
new HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>();
static {
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.KILOMETER, MeasureUnit.HOUR), MeasureUnit.KILOMETER_PER_HOUR);
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.GALLON), MeasureUnit.MILE_PER_GALLON);
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.HOUR), MeasureUnit.MILE_PER_HOUR);
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.METER, MeasureUnit.SECOND), MeasureUnit.METER_PER_SECOND);
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.LITER, MeasureUnit.KILOMETER), MeasureUnit.LITER_PER_KILOMETER);
unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.POUND, MeasureUnit.SQUARE_INCH), MeasureUnit.POUND_PER_SQUARE_INCH);
}
// End generated MeasureUnit constants
/* Private */

View file

@ -25,6 +25,7 @@ import java.util.TreeMap;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.serializable.SerializableTest;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.text.MeasureFormat;
@ -44,6 +45,28 @@ import com.ibm.icu.util.ULocale;
*/
public class MeasureUnitTest extends TestFmwk {
static class OrderedPair<F extends Comparable, S extends Comparable> extends Pair<F, S> implements Comparable<OrderedPair<F, S>> {
OrderedPair(F first, S second) {
super(first, second);
}
public static <F extends Comparable, S extends Comparable> OrderedPair<F, S> of(F first, S second) {
if (first == null || second == null) {
throw new IllegalArgumentException("OrderedPair.of requires non null values.");
}
return new OrderedPair<F, S>(first, second);
}
public int compareTo(OrderedPair<F, S> other) {
int result = first.compareTo(other.first);
if (result != 0) {
return result;
}
return second.compareTo(other.second);
}
}
private static final String[] DRAFT_VERSIONS = {"53", "54"};
private static final HashSet<String> DRAFT_VERSION_SET = new HashSet<String>();
@ -742,56 +765,59 @@ public class MeasureUnitTest extends TestFmwk {
}
}
public void testMultiplesPer() {
Object[][] data = new Object[][] {
// perUnit pattern
{ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.SECOND, "2 miles, 1 foot, 2.3 inches per second"},
{ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.SECOND, "2 mi, 1 ft, 2.3 inps"},
{ULocale.ENGLISH, FormatWidth.NARROW, MeasureUnit.SECOND, "2mi 1\u2032 2.3\u2033/s"},
// global per pattern
{ULocale.ENGLISH, FormatWidth.WIDE, MeasureUnit.MINUTE, "2 miles, 1 foot, 2.3 inches per minute"},
{ULocale.ENGLISH, FormatWidth.SHORT, MeasureUnit.MINUTE, "2 mi, 1 ft, 2.3 in/min"},
{ULocale.ENGLISH, FormatWidth.NARROW, MeasureUnit.MINUTE, "2mi 1\u2032 2.3\u2033/m"}
};
for (Object[] row : data) {
MeasureFormat mf = MeasureFormat.getInstance(
(ULocale) row[0], (FormatWidth) row[1]);
assertEquals(
"testMultiples",
row[3],
mf.formatMeasuresPer(
new StringBuilder(),
new FieldPosition(0),
(MeasureUnit) row[2],
new Measure(2, MeasureUnit.MILE),
new Measure(1, MeasureUnit.FOOT),
new Measure(2.3, MeasureUnit.INCH)).toString());
}
}
public void testSimplePer() {
Object DONT_CARE = null;
Object[][] data = new Object[][] {
// per unit singular
{1, MeasureUnit.SECOND, "1 lbps"},
// per unit plural
{2, MeasureUnit.SECOND, "2 lbps"},
// compound singular
{1, MeasureUnit.MINUTE, "1 lb/min"},
// compound plural
{2, MeasureUnit.MINUTE, "2 lb/min"},
// per unit pattern
{FormatWidth.WIDE, 1.0, MeasureUnit.SECOND, "1 pound per second", DONT_CARE, 0, 0},
{FormatWidth.WIDE, 2.0, MeasureUnit.SECOND, "2 pounds per second", DONT_CARE, 0, 0},
// compound pattern
{FormatWidth.WIDE, 1.0, MeasureUnit.MINUTE, "1 pound per minute", DONT_CARE, 0, 0},
{FormatWidth.WIDE, 2.0, MeasureUnit.MINUTE, "2 pounds per minute", DONT_CARE, 0, 0},
// per unit
{FormatWidth.SHORT, 1.0, MeasureUnit.SECOND, "1 lbps", DONT_CARE, 0, 0},
{FormatWidth.SHORT, 2.0, MeasureUnit.SECOND, "2 lbps", DONT_CARE, 0, 0},
// compound
{FormatWidth.SHORT, 1.0, MeasureUnit.MINUTE, "1 lb/min", DONT_CARE, 0, 0},
{FormatWidth.SHORT, 2.0, MeasureUnit.MINUTE, "2 lb/min", DONT_CARE, 0, 0},
// per unit
{FormatWidth.NARROW, 1.0, MeasureUnit.SECOND, "1#/s", DONT_CARE, 0, 0},
{FormatWidth.NARROW, 2.0, MeasureUnit.SECOND, "2#/s", DONT_CARE, 0, 0},
// compound
{FormatWidth.NARROW, 1.0, MeasureUnit.MINUTE, "1#/m", DONT_CARE, 0, 0},
{FormatWidth.NARROW, 2.0, MeasureUnit.MINUTE, "2#/m", DONT_CARE, 0, 0},
// field positions
{FormatWidth.SHORT, 23.3, MeasureUnit.SECOND, "23.3 lbps", NumberFormat.Field.DECIMAL_SEPARATOR, 2, 3},
{FormatWidth.SHORT, 23.3, MeasureUnit.SECOND, "23.3 lbps", NumberFormat.Field.INTEGER, 0, 2},
{FormatWidth.SHORT, 23.3, MeasureUnit.MINUTE, "23.3 lb/min", NumberFormat.Field.DECIMAL_SEPARATOR, 2, 3},
{FormatWidth.SHORT, 23.3, MeasureUnit.MINUTE, "23.3 lb/min", NumberFormat.Field.INTEGER, 0, 2},
};
for (Object[] row : data) {
FormatWidth formatWidth = (FormatWidth) row[0];
Number amount = (Number) row[1];
MeasureUnit perUnit = (MeasureUnit) row[2];
String expected = row[3].toString();
NumberFormat.Field field = (NumberFormat.Field) row[4];
int startOffset = ((Integer) row[5]).intValue();
int endOffset = ((Integer) row[6]).intValue();
MeasureFormat mf = MeasureFormat.getInstance(
ULocale.ENGLISH, FormatWidth.SHORT);
ULocale.ENGLISH, formatWidth);
FieldPosition pos = field != null ? new FieldPosition(field) : new FieldPosition(0);
String prefix = "Prefix: ";
assertEquals(
"",
row[2],
mf.formatMeasuresPer(
new StringBuilder(),
new FieldPosition(0),
(MeasureUnit) row[1],
new Measure((Number) row[0], MeasureUnit.POUND)).toString());
prefix + expected,
mf.formatMeasurePerUnit(
new Measure(amount, MeasureUnit.POUND),
perUnit,
new StringBuilder(prefix),
pos).toString());
if (field != DONT_CARE) {
assertEquals("startOffset", startOffset, pos.getBeginIndex() - prefix.length());
assertEquals("endOffset", endOffset, pos.getEndIndex() - prefix.length());
}
}
}
@ -808,11 +834,11 @@ public class MeasureUnitTest extends TestFmwk {
assertEquals(
"",
row[1],
mf.formatMeasuresPer(
new StringBuilder(),
new FieldPosition(0),
mf.formatMeasurePerUnit(
new Measure((Number) row[0], MeasureUnit.FOOT),
MeasureUnit.SECOND,
new Measure((Number) row[0], MeasureUnit.FOOT)).toString());
new StringBuilder(),
new FieldPosition(0)).toString());
}
}
@ -929,64 +955,6 @@ public class MeasureUnitTest extends TestFmwk {
}
public void testFieldPositionMultipleWithPer() {
MeasureFormat fmt = MeasureFormat.getInstance(
ULocale.ENGLISH, FormatWidth.SHORT);
FieldPosition pos = new FieldPosition(NumberFormat.Field.INTEGER);
String result = fmt.formatMeasuresPer(
new StringBuilder(),
pos,
MeasureUnit.SECOND,
new Measure(354, MeasureUnit.METER),
new Measure(23, MeasureUnit.CENTIMETER)).toString();
assertEquals("result", "354 m, 23 cmps", result);
// According to javadocs for {@link Format#format} FieldPosition is set to
// beginning and end of first such field encountered instead of the last
// such field encountered.
assertEquals("beginIndex", 0, pos.getBeginIndex());
assertEquals("endIndex", 3, pos.getEndIndex());
pos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
result = fmt.formatMeasuresPer(
new StringBuilder("123456: "),
pos,
MeasureUnit.SECOND,
new Measure(354, MeasureUnit.METER),
new Measure(23, MeasureUnit.CENTIMETER),
new Measure(5.4, MeasureUnit.MILLIMETER)).toString();
assertEquals("result", "123456: 354 m, 23 cm, 5.4 mmps", result);
assertEquals("beginIndex", 23, pos.getBeginIndex());
assertEquals("endIndex", 24, pos.getEndIndex());
pos = new FieldPosition(NumberFormat.Field.INTEGER);
result = fmt.formatMeasuresPer(
new StringBuilder(),
pos,
MeasureUnit.MINUTE,
new Measure(354, MeasureUnit.METER),
new Measure(23, MeasureUnit.CENTIMETER)).toString();
assertEquals("result", "354 m, 23 cm/min", result);
// According to javadocs for {@link Format#format} FieldPosition is set to
// beginning and end of first such field encountered instead of the last
// such field encountered.
assertEquals("beginIndex", 0, pos.getBeginIndex());
assertEquals("endIndex", 3, pos.getEndIndex());
pos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
result = fmt.formatMeasuresPer(
new StringBuilder("123456: "),
pos,
MeasureUnit.MINUTE,
new Measure(354, MeasureUnit.METER),
new Measure(23, MeasureUnit.CENTIMETER),
new Measure(5.4, MeasureUnit.MILLIMETER)).toString();
assertEquals("result", "123456: 354 m, 23 cm, 5.4 mm/min", result);
assertEquals("beginIndex", 23, pos.getBeginIndex());
assertEquals("endIndex", 24, pos.getEndIndex());
}
public void testOldFormatWithList() {
List<Measure> measures = new ArrayList<Measure>(2);
measures.add(new Measure(5, MeasureUnit.ACRE));
@ -1027,6 +995,19 @@ public class MeasureUnitTest extends TestFmwk {
}
}
public void testUnitPerUnitResolution() {
// Ticket 11274
MeasureFormat fmt = MeasureFormat.getInstance(Locale.ENGLISH, FormatWidth.SHORT);
// This fails unless we resolve to MeasureUnit.POUND_PER_SQUARE_INCH
assertEquals("", "50 psi",
fmt.formatMeasurePerUnit(
new Measure(50, MeasureUnit.POUND),
MeasureUnit.SQUARE_INCH,
new StringBuilder(),
new FieldPosition(0)).toString());
}
public void testEqHashCode() {
MeasureFormat mf = MeasureFormat.getInstance(ULocale.CANADA, FormatWidth.SHORT);
MeasureFormat mfeq = MeasureFormat.getInstance(ULocale.CANADA, FormatWidth.SHORT);
@ -1108,6 +1089,42 @@ public class MeasureUnitTest extends TestFmwk {
}
// DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
// for MeasureFormat during the release process.
static Map<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> getUnitsToPerParts() {
TreeMap<String, List<MeasureUnit>> allUnits = getAllUnits();
Map<MeasureUnit, Pair<String, String>> unitsToPerStrings =
new HashMap<MeasureUnit, Pair<String, String>>();
Map<String, MeasureUnit> namesToUnits = new HashMap<String, MeasureUnit>();
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
String type = entry.getKey();
// Currency types are always atomic units, so we can skip these
if (type.equals("currency")) {
continue;
}
for (MeasureUnit unit : entry.getValue()) {
String javaName = toJAVAName(unit);
String[] nameParts = javaName.split("_PER_");
if (nameParts.length == 1) {
namesToUnits.put(nameParts[0], unit);
} else if (nameParts.length == 2) {
unitsToPerStrings.put(unit, Pair.of(nameParts[0], nameParts[1]));
}
}
}
Map<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> unitsToPerUnits =
new HashMap<MeasureUnit, Pair<MeasureUnit, MeasureUnit>>();
for (Map.Entry<MeasureUnit, Pair<String, String>> entry : unitsToPerStrings.entrySet()) {
Pair<String, String> perStrings = entry.getValue();
MeasureUnit unit = namesToUnits.get(perStrings.first);
MeasureUnit perUnit = namesToUnits.get(perStrings.second);
if (unit != null && perUnit != null) {
unitsToPerUnits.put(entry.getKey(), Pair.of(unit, perUnit));
}
}
return unitsToPerUnits;
}
// DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
// for MeasureFormat during the release process.
static void generateCXXHConstants(String thisVersion) {
@ -1196,9 +1213,9 @@ public class MeasureUnitTest extends TestFmwk {
// DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
// for MeasureFormat during the release process.
static void generateCXXConstants() {
System.out.println("static final MeasureUnit");
Map<String, MeasureUnit> seen = new HashMap<String, MeasureUnit>();
System.out.println("");
TreeMap<String, List<MeasureUnit>> allUnits = getAllUnits();
System.out.println("static const int32_t gOffsets[] = {");
int index = 0;
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
@ -1219,6 +1236,7 @@ public class MeasureUnitTest extends TestFmwk {
System.out.printf(" %d\n", index);
System.out.println("};");
System.out.println();
System.out.println("// Must be sorted alphabetically.");
System.out.println("static const char * const gTypes[] = {");
boolean first = true;
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
@ -1231,47 +1249,89 @@ public class MeasureUnitTest extends TestFmwk {
System.out.println();
System.out.println("};");
System.out.println();
System.out.println("// Must be grouped by type and sorted alphabetically within each type.");
System.out.println("static const char * const gSubTypes[] = {");
first = true;
int offset = 0;
int typeIdx = 0;
Map<MeasureUnit, Integer> measureUnitToOffset = new HashMap<MeasureUnit, Integer>();
Map<MeasureUnit, Pair<Integer, Integer>> measureUnitToTypeSubType =
new HashMap<MeasureUnit, Pair<Integer, Integer>>();
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
int subTypeIdx = 0;
for (MeasureUnit unit : entry.getValue()) {
if (!first) {
System.out.println(",");
}
System.out.print(" \"" + unit.getSubtype() + "\"");
first = false;
measureUnitToOffset.put(unit, offset);
measureUnitToTypeSubType.put(unit, Pair.of(typeIdx, subTypeIdx));
offset++;
subTypeIdx++;
}
typeIdx++;
}
System.out.println();
System.out.println("};");
System.out.println();
int typeIdx = 0;
// Build unit per unit offsets to corresponding type sub types sorted by
// unit first and then per unit.
TreeMap<OrderedPair<Integer, Integer>, Pair<Integer, Integer>> unitPerUnitOffsetsToTypeSubType
= new TreeMap<OrderedPair<Integer, Integer>, Pair<Integer, Integer>>();
for (Map.Entry<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> entry
: getUnitsToPerParts().entrySet()) {
Pair<MeasureUnit, MeasureUnit> unitPerUnit = entry.getValue();
unitPerUnitOffsetsToTypeSubType.put(
OrderedPair.of(
measureUnitToOffset.get(unitPerUnit.first),
measureUnitToOffset.get(unitPerUnit.second)),
measureUnitToTypeSubType.get(entry.getKey()));
}
System.out.println("// Must be sorted by first value and then second value.");
System.out.println("static int32_t unitPerUnitToSingleUnit[][4] = {");
first = true;
for (Map.Entry<OrderedPair<Integer, Integer>, Pair<Integer, Integer>> entry
: unitPerUnitOffsetsToTypeSubType.entrySet()) {
if (!first) {
System.out.println(",");
}
first = false;
OrderedPair<Integer, Integer> unitPerUnitOffsets = entry.getKey();
Pair<Integer, Integer> typeSubType = entry.getValue();
System.out.printf(" {%d, %d, %d, %d}",
unitPerUnitOffsets.first,
unitPerUnitOffsets.second,
typeSubType.first,
typeSubType.second);
}
System.out.println();
System.out.println("};");
System.out.println();
Map<String, MeasureUnit> seen = new HashMap<String, MeasureUnit>();
for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
int subTypeIdx = 0;
String type = entry.getKey();
if (type.equals("currency")) {
typeIdx++;
continue;
}
for (MeasureUnit unit : entry.getValue()) {
String name = toCamelCase(unit);
String javaName = toJAVAName(unit);
Pair<Integer, Integer> typeSubType = measureUnitToTypeSubType.get(unit);
if (typeSubType == null) {
throw new IllegalStateException();
}
checkForDup(seen, name, unit);
if (isDraft(javaName)) {
System.out.println("#ifndef U_HIDE_DRAFT_API");
}
System.out.printf("MeasureUnit *MeasureUnit::create%s(UErrorCode &status) {\n", name);
System.out.printf(" return MeasureUnit::create(%d, %d, status);\n", typeIdx, subTypeIdx);
System.out.printf(" return MeasureUnit::create(%d, %d, status);\n",
typeSubType.first, typeSubType.second);
System.out.println("}");
if (isDraft(javaName)) {
System.out.println("#endif /* U_HIDE_DRAFT_API */");
}
System.out.println();
subTypeIdx++;
System.out.println();
}
typeIdx++;
}
}
}
private static String toCamelCase(MeasureUnit unit) {
@ -1403,6 +1463,16 @@ public class MeasureUnitTest extends TestFmwk {
System.out.println();
}
}
System.out.println(" private static HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>unitPerUnitToSingleUnit =");
System.out.println(" new HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>();");
System.out.println();
System.out.println(" static {");
for (Map.Entry<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> unitPerUnitEntry
: getUnitsToPerParts().entrySet()) {
Pair<MeasureUnit, MeasureUnit> unitPerUnit = unitPerUnitEntry.getValue();
System.out.println(" unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit." + toJAVAName(unitPerUnit.first) + ", MeasureUnit." + toJAVAName(unitPerUnit.second) + "), MeasureUnit." + toJAVAName(unitPerUnitEntry.getKey()) + ");");
}
System.out.println(" }");
}
private static String getVersion(String javaName, String thisVersion) {

View file

@ -48,7 +48,7 @@ public class SimplePatternFormatterTest extends TestFmwk {
assertEquals(
"toString2",
"This doesn't have templates {0}",
fmt.format(new StringBuilder(), offsets).toString());
fmt.formatAndAppend(new StringBuilder(), offsets).toString());
assertEquals(
"offsets[0]",
-1,
@ -78,20 +78,38 @@ public class SimplePatternFormatterTest extends TestFmwk {
"Templates {1}{2} and {3} are here.").getPatternWithNoPlaceholders());
}
public void TestWithPlaceholders() {
public void TestTooFewPlaceholderValues() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile(
"Templates {2}{1} and {4} are out of order.");
assertFalse("startsWithPlaceholder", fmt.startsWithPlaceholder(2));
assertEquals(
"getPlaceholderCount",
5,
fmt.getPlaceholderCount());
try {
fmt.format("freddy", "tommy", "frog", "leg");
fail("Expected UnsupportedOperationException");
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected
}
try {
fmt.formatAndAppend(
new StringBuilder(), null, "freddy", "tommy", "frog", "leg");
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected
}
try {
fmt.formatAndReplace(
new StringBuilder(), null, "freddy", "tommy", "frog", "leg");
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected
}
}
public void TestWithPlaceholders() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile(
"Templates {2}{1} and {4} are out of order.");
assertEquals(
"getPlaceholderCount",
5,
fmt.getPlaceholderCount());
assertEquals(
"toString",
"Templates {2}{1} and {4} are out of order.",
@ -100,38 +118,121 @@ public class SimplePatternFormatterTest extends TestFmwk {
assertEquals(
"format",
"123456: Templates frogtommy and {0} are out of order.",
fmt.format(
fmt.formatAndAppend(
new StringBuilder("123456: "),
offsets,
"freddy", "tommy", "frog", "leg", "{0}").toString());
int[] expectedOffsets = {-1, 22, 18, -1, 32, -1};
for (int i = 0; i < offsets.length; i++) {
if (offsets[i] != expectedOffsets[i]) {
fail("getOffset() returned wrong value for " + i);
}
verifyOffsets(expectedOffsets, offsets);
}
public void TestFormatUseAppendToAsPlaceholder() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile(
"Placeholders {0} and {1}");
StringBuilder appendTo = new StringBuilder("previous:");
try {
fmt.formatAndAppend(appendTo, null, appendTo, "frog");
fail("IllegalArgumentException expected.");
} catch (IllegalArgumentException e) {
// expected.
}
}
public void TestOptimization() {
public void TestFormatReplaceNoOptimization() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}");
assertTrue("startsWithPlaceholder", fmt.startsWithPlaceholder(2));
assertFalse("startsWithPlaceholder", fmt.startsWithPlaceholder(0));
int[] offsets = new int[4];
StringBuilder appendTo = new StringBuilder("leg");
StringBuilder result = new StringBuilder("original");
assertEquals(
"format",
"leg, freddy, frog and by",
fmt.format(
appendTo,
"frog, original, freddy and by",
fmt.formatAndReplace(
result,
offsets,
"freddy", "frog", appendTo, "by").toString());
result, "freddy", "frog", "by").toString());
int[] expectedOffsets = {5, 13, 0, 22};
for (int i = 0; i < offsets.length; i++) {
if (offsets[i] != expectedOffsets[i]) {
fail("getOffset() returned wrong value for " + i);
}
}
int[] expectedOffsets = {6, 16, 0, 27};
verifyOffsets(expectedOffsets, offsets);
}
public void TestFormatReplaceNoOptimizationLeadingText() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile("boo {2}, {0}, {1} and {3}");
int[] offsets = new int[4];
StringBuilder result = new StringBuilder("original");
assertEquals(
"format",
"boo original, freddy, frog and by",
fmt.formatAndReplace(
result,
offsets,
"freddy", "frog", result, "by").toString());
int[] expectedOffsets = {14, 22, 4, 31};
verifyOffsets(expectedOffsets, offsets);
}
public void TestFormatReplaceOptimization() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}");
int[] offsets = new int[4];
StringBuilder result = new StringBuilder("original");
assertEquals(
"format",
"original, freddy, frog and by",
fmt.formatAndReplace(
result,
offsets,
"freddy", "frog", result, "by").toString());
int[] expectedOffsets = {10, 18, 0, 27};
verifyOffsets(expectedOffsets, offsets);
}
public void TestFormatReplaceOptimizationNoOffsets() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile("{2}, {0}, {1} and {3}");
StringBuilder result = new StringBuilder("original");
assertEquals(
"format",
"original, freddy, frog and by",
fmt.formatAndReplace(
result,
null,
"freddy", "frog", result, "by").toString());
}
public void TestFormatReplaceNoOptimizationNoOffsets() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile(
"Placeholders {0} and {1}");
StringBuilder result = new StringBuilder("previous:");
assertEquals(
"",
"Placeholders previous: and frog",
fmt.formatAndReplace(result, null, result, "frog").toString());
}
public void TestFormatReplaceNoOptimizationLeadingPlaceholderUsedTwice() {
SimplePatternFormatter fmt = SimplePatternFormatter.compile(
"{2}, {0}, {1} and {3} {2}");
StringBuilder result = new StringBuilder("original");
int[] offsets = new int[4];
assertEquals(
"",
"original, freddy, frog and by original",
fmt.formatAndReplace(
result,
offsets,
"freddy", "frog", result, "by").toString());
int[] expectedOffsets = {10, 18, 30, 27};
verifyOffsets(expectedOffsets, offsets);
}
void verifyOffsets(int[] expected, int[] actual) {
for (int i = 0; i < expected.length; ++i) {
if (expected[i] != actual[i]) {
errln("Expected "+expected[i]+", got " + actual[i]);
}
}
}
}