|
18 | 18 | import com.oracle.truffle.api.nodes.ExplodeLoop;
|
19 | 19 | import com.oracle.truffle.api.profiles.BranchProfile;
|
20 | 20 | import com.oracle.truffle.api.profiles.ConditionProfile;
|
| 21 | + |
21 | 22 | import org.truffleruby.SuppressFBWarnings;
|
22 | 23 | import org.truffleruby.builtins.CoreMethod;
|
23 | 24 | import org.truffleruby.builtins.CoreMethodArrayArgumentsNode;
|
|
29 | 30 | import org.truffleruby.core.encoding.Encodings;
|
30 | 31 | import org.truffleruby.core.numeric.FloatNodesFactory.ModNodeFactory;
|
31 | 32 | import org.truffleruby.core.rope.CodeRange;
|
| 33 | +import org.truffleruby.core.rope.Rope; |
| 34 | +import org.truffleruby.core.rope.RopeOperations; |
32 | 35 | import org.truffleruby.core.string.RubyString;
|
33 | 36 | import org.truffleruby.core.string.StringNodes;
|
34 | 37 | import org.truffleruby.core.string.StringUtils;
|
| 38 | +import org.truffleruby.core.thread.RubyThread; |
| 39 | +import org.truffleruby.language.NotProvided; |
35 | 40 | import org.truffleruby.language.RubyDynamicObject;
|
36 | 41 | import org.truffleruby.language.Visibility;
|
37 | 42 | import org.truffleruby.language.control.RaiseException;
|
38 | 43 | import org.truffleruby.language.dispatch.DispatchNode;
|
39 | 44 |
|
| 45 | +import java.text.DecimalFormat; |
| 46 | +import java.text.DecimalFormatSymbols; |
40 | 47 | import java.util.Locale;
|
41 | 48 |
|
42 | 49 | @CoreModule(value = "Float", isClass = true)
|
@@ -826,62 +833,109 @@ protected double toF(double value) {
|
826 | 833 | }
|
827 | 834 |
|
828 | 835 | @CoreMethod(names = { "to_s", "inspect" })
|
| 836 | + @ImportStatic(Double.class) |
829 | 837 | public abstract static class ToSNode extends CoreMethodArrayArgumentsNode {
|
830 | 838 |
|
831 | 839 | @Child private StringNodes.MakeStringNode makeStringNode = StringNodes.MakeStringNode.create();
|
832 | 840 |
|
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. */ |
| 841 | + /* Ruby has complex custom formatting logic for floats. Our logic meets the specs but we suspect it's possibly |
| 842 | + * still not entirely correct. JRuby seems to be correct, but their logic is tied up in their printf |
| 843 | + * implementation. Also see our FormatFloatNode, which I suspect is also deficient or under-tested. */ |
840 | 844 |
|
841 |
| - if (Double.isInfinite(value) || Double.isNaN(value)) { |
842 |
| - return makeStringNode.executeMake(Double.toString(value), Encodings.US_ASCII, CodeRange.CR_7BIT); |
843 |
| - } |
| 845 | + @Specialization(guards = "value == POSITIVE_INFINITY") |
| 846 | + protected RubyString toSPositiveInfinity(double value, |
| 847 | + @Cached("specialValueRope(POSITIVE_INFINITY)") Rope cachedRope) { |
| 848 | + return makeStringNode.executeMake(cachedRope, Encodings.US_ASCII, NotProvided.INSTANCE); |
| 849 | + } |
844 | 850 |
|
845 |
| - String str = StringUtils.format(Locale.ENGLISH, "%.17g", value); |
| 851 | + @Specialization(guards = "value == NEGATIVE_INFINITY") |
| 852 | + protected RubyString toSNegativeInfinity(double value, |
| 853 | + @Cached("specialValueRope(NEGATIVE_INFINITY)") Rope cachedRope) { |
| 854 | + return makeStringNode.executeMake(cachedRope, Encodings.US_ASCII, NotProvided.INSTANCE); |
| 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 = "isNaN(value)") |
| 858 | + protected RubyString toSNaN(double value, |
| 859 | + @Cached("specialValueRope(value)") 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 = "hasNoExp(value)") |
| 864 | + protected RubyString toSNoExp(double value) { |
| 865 | + return makeStringNode.executeMake(makeRopeNoExp(value, getLanguage().getCurrentThread()), |
| 866 | + Encodings.US_ASCII, CodeRange.CR_7BIT); |
| 867 | + } |
855 | 868 |
|
856 |
| - final int e = str.indexOf('e'); |
857 |
| - final boolean hasE = e != -1; |
| 869 | + @Specialization(guards = "hasLargeExp(value)") |
| 870 | + protected RubyString toSLargeExp(double value) { |
| 871 | + return makeStringNode.executeMake(makeRopeLargeExp(value, getLanguage().getCurrentThread()), |
| 872 | + Encodings.US_ASCII, CodeRange.CR_7BIT); |
| 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 = "hasSmallExp(value)") |
| 876 | + protected RubyString toSSmallExp(double value) { |
| 877 | + return makeStringNode.executeMake(makeRopeSmallExp(value, getLanguage().getCurrentThread()), |
| 878 | + Encodings.US_ASCII, CodeRange.CR_7BIT); |
| 879 | + } |
| 880 | + |
| 881 | + @TruffleBoundary |
| 882 | + private String makeRopeNoExp(double value, RubyThread thread) { |
| 883 | + return getNoExpFormat(thread).format(value); |
| 884 | + } |
865 | 885 |
|
866 |
| - String formatted = str.substring(0, i + 1) + str.substring(start); |
| 886 | + @TruffleBoundary |
| 887 | + private String makeRopeSmallExp(double value, RubyThread thread) { |
| 888 | + return getSmallExpFormat(thread).format(value); |
| 889 | + } |
| 890 | + |
| 891 | + @TruffleBoundary |
| 892 | + private String makeRopeLargeExp(double value, RubyThread thread) { |
| 893 | + return getLargeExpFormat(thread).format(value); |
| 894 | + } |
867 | 895 |
|
868 |
| - int wholeDigits = 0; |
869 |
| - int n = 0; |
| 896 | + protected static boolean hasNoExp(double value) { |
| 897 | + double abs = Math.abs(value); |
| 898 | + return abs == 0.0 || ((abs >= 0.0001) && (abs < 1_000_000_000_000_000.0)); |
| 899 | + } |
870 | 900 |
|
871 |
| - if (formatted.charAt(0) == '-') { |
872 |
| - n++; |
873 |
| - } |
| 901 | + protected static boolean hasLargeExp(double value) { |
| 902 | + double abs = Math.abs(value); |
| 903 | + return Double.isFinite(abs) && (abs >= 1_000_000_000_000_000.0); |
| 904 | + } |
874 | 905 |
|
875 |
| - while (formatted.charAt(n) != '.') { |
876 |
| - wholeDigits++; |
877 |
| - n++; |
| 906 | + protected static boolean hasSmallExp(double value) { |
| 907 | + double abs = Math.abs(value); |
| 908 | + return (abs < 0.0001) && (abs != 0.0); |
| 909 | + } |
| 910 | + |
| 911 | + protected static Rope specialValueRope(double value) { |
| 912 | + return RopeOperations.encodeAscii(Double.toString(value), Encodings.US_ASCII.jcoding); |
| 913 | + } |
| 914 | + |
| 915 | + private DecimalFormat getNoExpFormat(RubyThread thread) { |
| 916 | + if (thread.noExpFormat == null) { |
| 917 | + final DecimalFormatSymbols noExpSymbols = new DecimalFormatSymbols(Locale.ENGLISH); |
| 918 | + thread.noExpFormat = new DecimalFormat("0.0################", noExpSymbols); |
878 | 919 | }
|
| 920 | + return thread.noExpFormat; |
| 921 | + } |
879 | 922 |
|
880 |
| - if (wholeDigits >= 16) { |
881 |
| - formatted = StringUtils.format(Locale.ENGLISH, "%.1e", value); |
| 923 | + private DecimalFormat getSmallExpFormat(RubyThread thread) { |
| 924 | + if (thread.smallExpFormat == null) { |
| 925 | + final DecimalFormatSymbols smallExpSymbols = new DecimalFormatSymbols(Locale.ENGLISH); |
| 926 | + smallExpSymbols.setExponentSeparator("e"); |
| 927 | + thread.smallExpFormat = new DecimalFormat("0.0################E00", smallExpSymbols); |
882 | 928 | }
|
| 929 | + return thread.smallExpFormat; |
| 930 | + } |
883 | 931 |
|
884 |
| - return makeStringNode.executeMake(formatted, Encodings.US_ASCII, CodeRange.CR_7BIT); |
| 932 | + private DecimalFormat getLargeExpFormat(RubyThread thread) { |
| 933 | + if (thread.largeExpFormat == null) { |
| 934 | + final DecimalFormatSymbols largeExpSymbols = new DecimalFormatSymbols(Locale.ENGLISH); |
| 935 | + largeExpSymbols.setExponentSeparator("e+"); |
| 936 | + thread.largeExpFormat = new DecimalFormat("0.0################E00", largeExpSymbols); |
| 937 | + } |
| 938 | + return thread.largeExpFormat; |
885 | 939 | }
|
886 | 940 |
|
887 | 941 | }
|
|
0 commit comments