Skip to content

Commit d49cf63

Browse files
committed
Reimplement Float#to_s using java.text.DecimalFormats.
1 parent fdabe1f commit d49cf63

File tree

2 files changed

+87
-40
lines changed

2 files changed

+87
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Compatibility:
1515

1616
Performance:
1717

18+
* Reimplement `Float#to_s` for better performance (#1584, @aardvark179).
1819

1920
Changes:
2021

src/main/java/org/truffleruby/core/numeric/FloatNodes.java

Lines changed: 86 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@
2929
import org.truffleruby.core.encoding.Encodings;
3030
import org.truffleruby.core.numeric.FloatNodesFactory.ModNodeFactory;
3131
import org.truffleruby.core.rope.CodeRange;
32+
import org.truffleruby.core.rope.Rope;
33+
import org.truffleruby.core.rope.RopeOperations;
3234
import org.truffleruby.core.string.RubyString;
3335
import org.truffleruby.core.string.StringNodes;
3436
import org.truffleruby.core.string.StringUtils;
37+
import org.truffleruby.language.NotProvided;
3538
import org.truffleruby.language.RubyDynamicObject;
3639
import org.truffleruby.language.Visibility;
3740
import org.truffleruby.language.control.RaiseException;
3841
import org.truffleruby.language.dispatch.DispatchNode;
3942

43+
import java.text.DecimalFormat;
44+
import java.text.DecimalFormatSymbols;
4045
import java.util.Locale;
4146

4247
@CoreModule(value = "Float", isClass = true)
@@ -826,64 +831,105 @@ protected double toF(double value) {
826831
}
827832

828833
@CoreMethod(names = { "to_s", "inspect" })
834+
@ImportStatic(Double.class)
829835
public abstract static class ToSNode extends CoreMethodArrayArgumentsNode {
830836

831837
@Child private StringNodes.MakeStringNode makeStringNode = StringNodes.MakeStringNode.create();
832838

833-
@TruffleBoundary
834-
@Specialization
835-
protected RubyString toS(double value) {
836-
/* Ruby has complex custom formatting logic for floats. Our logic meets the specs but we suspect it's
837-
* possibly still not entirely correct. JRuby seems to be correct, but their logic is tied up in their
838-
* printf implementation. Also see our FormatFloatNode, which I suspect is also deficient or
839-
* under-tested. */
839+
/* Ruby has complex custom formatting logic for floats. Our logic meets the specs but we suspect it's possibly
840+
* still not entirely correct. JRuby seems to be correct, but their logic is tied up in their printf
841+
* implementation. Also see our FormatFloatNode, which I suspect is also deficient or under-tested. */
840842

841-
if (Double.isInfinite(value) || Double.isNaN(value)) {
842-
return makeStringNode.executeMake(Double.toString(value), Encodings.US_ASCII, CodeRange.CR_7BIT);
843-
}
843+
private static final DecimalFormat DF_NO_EXP;
844+
private static final DecimalFormat DF_SMALL_EXP;
845+
private static final DecimalFormat DF_LARGE_EXP;
844846

845-
String str = StringUtils.format(Locale.ENGLISH, "%.17g", value);
847+
static {
848+
final DecimalFormatSymbols smallExpSymbols = new DecimalFormatSymbols(Locale.ENGLISH);
849+
smallExpSymbols.setExponentSeparator("e");
850+
final DecimalFormatSymbols largeExpSymbols = new DecimalFormatSymbols(Locale.ENGLISH);
851+
largeExpSymbols.setExponentSeparator("e+");
852+
DF_NO_EXP = new DecimalFormat("0.0################");
853+
DF_SMALL_EXP = new DecimalFormat("0.0################E00", smallExpSymbols);
854+
DF_LARGE_EXP = new DecimalFormat("0.0################E00", largeExpSymbols);
855+
}
846856

847-
// If no dot, add one to show it's a floating point number
848-
if (str.indexOf('.') == -1) {
849-
assert str.indexOf('e') == -1;
850-
str += ".0";
851-
}
857+
@Specialization(guards = "value == POSITIVE_INFINITY")
858+
protected RubyString toSPositiveInfinity(double value,
859+
@Cached("specialValueRope(POSITIVE_INFINITY)") Rope cachedRope) {
860+
return makeStringNode.executeMake(cachedRope, Encodings.US_ASCII, NotProvided.INSTANCE);
861+
}
852862

853-
final int dot = str.indexOf('.');
854-
assert dot != -1;
863+
@Specialization(guards = "value == NEGATIVE_INFINITY")
864+
protected RubyString toSNegativeInfinity(double value,
865+
@Cached("specialValueRope(NEGATIVE_INFINITY)") Rope cachedRope) {
866+
return makeStringNode.executeMake(cachedRope, Encodings.US_ASCII, NotProvided.INSTANCE);
867+
}
855868

856-
final int e = str.indexOf('e');
857-
final boolean hasE = e != -1;
869+
@Specialization(guards = "isNaN(value)")
870+
protected RubyString toSNaN(double value,
871+
@Cached("specialValueRope(value)") Rope cachedRope) {
872+
return makeStringNode.executeMake(cachedRope, Encodings.US_ASCII, NotProvided.INSTANCE);
873+
}
858874

859-
// Remove trailing zeroes, but keep at least one after the dot
860-
final int start = hasE ? e : str.length();
861-
int i = start - 1; // last digit we keep, inclusive
862-
while (i > dot + 1 && str.charAt(i) == '0') {
863-
i--;
864-
}
875+
@Specialization(guards = "hasNoExp(value)")
876+
protected RubyString toSNoExp(double value) {
877+
return makeStringNode.executeMake(makeRopeNoExp(value), Encodings.US_ASCII, NotProvided.INSTANCE);
878+
}
865879

866-
String formatted = str.substring(0, i + 1) + str.substring(start);
880+
@Specialization(guards = "hasLargeExp(value)")
881+
protected RubyString toSLargeExp(double value) {
882+
return makeStringNode.executeMake(makeRopeLargeExp(value), Encodings.US_ASCII, NotProvided.INSTANCE);
883+
}
867884

868-
int wholeDigits = 0;
869-
int n = 0;
885+
@Specialization(guards = "hasSmallExp(value)")
886+
protected RubyString toSSmallExp(double value) {
887+
return makeStringNode.executeMake(makeRopeSmallExp(value), Encodings.US_ASCII, NotProvided.INSTANCE);
888+
}
870889

871-
if (formatted.charAt(0) == '-') {
872-
n++;
873-
}
890+
@TruffleBoundary
891+
private Rope makeRopeSimple(double value) {
892+
String str = Double.toString(value);
874893

875-
while (formatted.charAt(n) != '.') {
876-
wholeDigits++;
877-
n++;
878-
}
894+
return RopeOperations.encodeAscii(str, Encodings.US_ASCII.jcoding);
895+
}
879896

880-
if (wholeDigits >= 16) {
881-
formatted = StringUtils.format(Locale.ENGLISH, "%.1e", value);
882-
}
897+
@TruffleBoundary
898+
private Rope makeRopeNoExp(double value) {
899+
String str = DF_NO_EXP.format(value);
900+
return RopeOperations.encodeAscii(str, Encodings.US_ASCII.jcoding);
901+
}
883902

884-
return makeStringNode.executeMake(formatted, Encodings.US_ASCII, CodeRange.CR_7BIT);
903+
@TruffleBoundary
904+
private Rope makeRopeSmallExp(double value) {
905+
String str = DF_SMALL_EXP.format(value);
906+
return RopeOperations.encodeAscii(str, Encodings.US_ASCII.jcoding);
907+
}
908+
909+
@TruffleBoundary
910+
private Rope makeRopeLargeExp(double value) {
911+
String str = DF_LARGE_EXP.format(value);
912+
return RopeOperations.encodeAscii(str, Encodings.US_ASCII.jcoding);
885913
}
886914

915+
protected static boolean hasNoExp(double value) {
916+
double abs = Math.abs(value);
917+
return abs == 0.0d || ((abs >= 0.0001d) && (abs < 1_000_000_000_000_000.0d));
918+
}
919+
920+
protected static boolean hasLargeExp(double value) {
921+
double abs = Math.abs(value);
922+
return (abs >= 1_000_000_000_000_000.0d);
923+
}
924+
925+
protected static boolean hasSmallExp(double value) {
926+
double abs = Math.abs(value);
927+
return (abs < 0.0001d) && (abs != 0.0d);
928+
}
929+
930+
protected static Rope specialValueRope(double value) {
931+
return RopeOperations.encodeAscii(Double.toString(value), Encodings.US_ASCII.jcoding);
932+
}
887933
}
888934

889935
@NonStandard

0 commit comments

Comments
 (0)