|
29 | 29 | import org.truffleruby.core.encoding.Encodings;
|
30 | 30 | import org.truffleruby.core.numeric.FloatNodesFactory.ModNodeFactory;
|
31 | 31 | import org.truffleruby.core.rope.CodeRange;
|
| 32 | +import org.truffleruby.core.rope.Rope; |
| 33 | +import org.truffleruby.core.rope.RopeOperations; |
32 | 34 | import org.truffleruby.core.string.RubyString;
|
33 | 35 | import org.truffleruby.core.string.StringNodes;
|
34 | 36 | import org.truffleruby.core.string.StringUtils;
|
| 37 | +import org.truffleruby.language.NotProvided; |
35 | 38 | import org.truffleruby.language.RubyDynamicObject;
|
36 | 39 | import org.truffleruby.language.Visibility;
|
37 | 40 | import org.truffleruby.language.control.RaiseException;
|
38 | 41 | import org.truffleruby.language.dispatch.DispatchNode;
|
39 | 42 |
|
| 43 | +import java.text.DecimalFormat; |
| 44 | +import java.text.DecimalFormatSymbols; |
40 | 45 | import java.util.Locale;
|
41 | 46 |
|
42 | 47 | @CoreModule(value = "Float", isClass = true)
|
@@ -826,64 +831,105 @@ protected double toF(double value) {
|
826 | 831 | }
|
827 | 832 |
|
828 | 833 | @CoreMethod(names = { "to_s", "inspect" })
|
| 834 | + @ImportStatic(Double.class) |
829 | 835 | public abstract static class ToSNode extends CoreMethodArrayArgumentsNode {
|
830 | 836 |
|
831 | 837 | @Child private StringNodes.MakeStringNode makeStringNode = StringNodes.MakeStringNode.create();
|
832 | 838 |
|
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. */ |
840 | 842 |
|
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; |
844 | 846 |
|
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 | + } |
846 | 856 |
|
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 | + } |
852 | 862 |
|
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 | + } |
855 | 868 |
|
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 | + } |
858 | 874 |
|
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 | + } |
865 | 879 |
|
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 | + } |
867 | 884 |
|
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 | + } |
870 | 889 |
|
871 |
| - if (formatted.charAt(0) == '-') { |
872 |
| - n++; |
873 |
| - } |
| 890 | + @TruffleBoundary |
| 891 | + private Rope makeRopeSimple(double value) { |
| 892 | + String str = Double.toString(value); |
874 | 893 |
|
875 |
| - while (formatted.charAt(n) != '.') { |
876 |
| - wholeDigits++; |
877 |
| - n++; |
878 |
| - } |
| 894 | + return RopeOperations.encodeAscii(str, Encodings.US_ASCII.jcoding); |
| 895 | + } |
879 | 896 |
|
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 | + } |
883 | 902 |
|
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); |
885 | 913 | }
|
886 | 914 |
|
| 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 | + } |
887 | 933 | }
|
888 | 934 |
|
889 | 935 | @NonStandard
|
|
0 commit comments