|
20 | 20 | import static com.google.common.base.Preconditions.checkNotNull;
|
21 | 21 | import static com.google.common.base.Strings.padEnd;
|
22 | 22 | import static com.google.common.base.Strings.padStart;
|
| 23 | +import static com.google.common.truth.Platform.doubleToString; |
| 24 | +import static com.google.common.truth.Platform.floatToString; |
23 | 25 | import static java.lang.Math.max;
|
24 | 26 |
|
25 | 27 | import com.google.common.collect.ImmutableList;
|
26 | 28 | import java.io.Serializable;
|
| 29 | +import java.math.BigDecimal; |
27 | 30 | import org.jspecify.annotations.Nullable;
|
28 | 31 |
|
29 | 32 | /**
|
@@ -68,29 +71,58 @@ public static Fact simpleFact(String key) {
|
68 | 71 | * Creates a fact with the given key and value, which will be printed in a format like "key:
|
69 | 72 | * value." The numeric value is converted to a string with delimiting commas.
|
70 | 73 | */
|
71 |
| - static Fact numericFact(String key, @Nullable Long value) { |
| 74 | + static Fact numericFact(String key, @Nullable Number value) { |
72 | 75 | return new Fact(key, formatNumericValue(value), true);
|
73 | 76 | }
|
74 | 77 |
|
75 | 78 | /**
|
76 |
| - * Creates a fact with the given key and value, which will be printed in a format like "key: |
77 |
| - * value." The numeric value is converted to a string with delimiting commas. |
| 79 | + * Formats the given numeric value as a string with delimiting commas. |
| 80 | + * |
| 81 | + * <p><b>Note:</b> only {@code Long}, {@code Integer}, {@code Float}, {@code Double} and {@link |
| 82 | + * BigDecimal} are supported. |
78 | 83 | */
|
79 |
| - static Fact numericFact(String key, @Nullable Integer value) { |
80 |
| - return new Fact(key, formatNumericValue(value), true); |
81 |
| - } |
82 |
| - |
83 |
| - static String formatNumericValue(@Nullable Object value) { |
| 84 | + static String formatNumericValue(@Nullable Number value) { |
84 | 85 | if (value == null) {
|
85 | 86 | return "null";
|
86 | 87 | }
|
| 88 | + // the value must be a numeric type |
| 89 | + checkArgument( |
| 90 | + value instanceof Long |
| 91 | + || value instanceof Integer |
| 92 | + || value instanceof Float |
| 93 | + || value instanceof Double |
| 94 | + || value instanceof BigDecimal, |
| 95 | + "Value (%s) must be either a Long, Integer, Float, Double, or BigDecimal.", |
| 96 | + value); |
| 97 | + |
| 98 | + // DecimalFormat is not available on all platforms, so we do the formatting manually. |
| 99 | + |
| 100 | + if (!(value instanceof BigDecimal) && isInfiniteOrNaN(value.doubleValue())) { |
| 101 | + return value.toString(); |
| 102 | + } |
| 103 | + String stringValue = |
| 104 | + value instanceof Double |
| 105 | + ? doubleToString((double) value) |
| 106 | + : value instanceof Float // |
| 107 | + ? floatToString((float) value) |
| 108 | + : value.toString(); |
| 109 | + if (stringValue.contains("E")) { |
| 110 | + return stringValue; |
| 111 | + } |
| 112 | + int decimalIndex = stringValue.indexOf('.'); |
| 113 | + if (decimalIndex == -1) { |
| 114 | + return formatWholeNumericValue(stringValue); |
| 115 | + } |
| 116 | + String wholeNumbers = stringValue.substring(0, decimalIndex); |
| 117 | + String decimal = stringValue.substring(decimalIndex); |
| 118 | + return formatWholeNumericValue(wholeNumbers) + decimal; |
| 119 | + } |
87 | 120 |
|
88 |
| - // We only support Long and Integer for now; maybe FP numbers in the future? |
89 |
| - checkArgument(value instanceof Long || value instanceof Integer); |
90 |
| - |
91 |
| - // DecimalFormat is not available on all platforms |
92 |
| - String stringValue = String.valueOf(value); |
| 121 | + private static boolean isInfiniteOrNaN(double d) { |
| 122 | + return Double.isInfinite(d) || Double.isNaN(d); |
| 123 | + } |
93 | 124 |
|
| 125 | + private static String formatWholeNumericValue(String stringValue) { |
94 | 126 | boolean isNegative = stringValue.startsWith("-");
|
95 | 127 | if (isNegative) {
|
96 | 128 | stringValue = stringValue.substring(1);
|
@@ -132,13 +164,18 @@ public String toString() {
|
132 | 164 | */
|
133 | 165 | static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> facts) {
|
134 | 166 | int longestKeyLength = 0;
|
135 |
| - int longestValueLength = 0; |
| 167 | + int longestIntPartValueLength = 0; |
136 | 168 | boolean seenNewlineInValue = false;
|
137 | 169 | for (Fact fact : facts) {
|
138 | 170 | if (fact.value != null) {
|
139 | 171 | longestKeyLength = max(longestKeyLength, fact.key.length());
|
140 | 172 | if (fact.padStart) {
|
141 |
| - longestValueLength = max(longestValueLength, fact.value.length()); |
| 173 | + int decimalIndex = fact.value.indexOf('.'); |
| 174 | + if (decimalIndex != -1) { |
| 175 | + longestIntPartValueLength = max(longestIntPartValueLength, decimalIndex); |
| 176 | + } else { |
| 177 | + longestIntPartValueLength = max(longestIntPartValueLength, fact.value.length()); |
| 178 | + } |
142 | 179 | }
|
143 | 180 | // TODO(cpovirk): Look for other kinds of newlines.
|
144 | 181 | seenNewlineInValue |= fact.value.contains("\n");
|
@@ -172,7 +209,14 @@ static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> fa
|
172 | 209 | builder.append(padEnd(fact.key, longestKeyLength, ' '));
|
173 | 210 | builder.append(": ");
|
174 | 211 | if (fact.padStart) {
|
175 |
| - builder.append(padStart(fact.value, longestValueLength, ' ')); |
| 212 | + int decimalIndex = fact.value.indexOf('.'); |
| 213 | + if (decimalIndex != -1) { |
| 214 | + builder.append( |
| 215 | + padStart(fact.value.substring(0, decimalIndex), longestIntPartValueLength, ' ')); |
| 216 | + builder.append(fact.value.substring(decimalIndex)); |
| 217 | + } else { |
| 218 | + builder.append(padStart(fact.value, longestIntPartValueLength, ' ')); |
| 219 | + } |
176 | 220 | } else {
|
177 | 221 | builder.append(fact.value);
|
178 | 222 | }
|
|
0 commit comments