aboutsummaryrefslogtreecommitdiff
path: root/src/jdk/nashorn/internal/objects/NativeNumber.java
blob: edb83f06d18397a6fb5095cdcace67c606f36103 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.objects;

import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
import static jdk.nashorn.internal.runtime.JSType.isRepresentableAsInt;
import static jdk.nashorn.internal.runtime.JSType.isRepresentableAsLong;
import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
import static jdk.nashorn.internal.lookup.Lookup.MH;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.text.NumberFormat;
import java.util.Locale;
import jdk.internal.dynalink.linker.GuardedInvocation;
import jdk.internal.dynalink.linker.LinkRequest;
import jdk.nashorn.internal.objects.annotations.Attribute;
import jdk.nashorn.internal.objects.annotations.Constructor;
import jdk.nashorn.internal.objects.annotations.Function;
import jdk.nashorn.internal.objects.annotations.Property;
import jdk.nashorn.internal.objects.annotations.ScriptClass;
import jdk.nashorn.internal.objects.annotations.Where;
import jdk.nashorn.internal.runtime.JSType;
import jdk.nashorn.internal.runtime.ScriptObject;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import jdk.nashorn.internal.lookup.MethodHandleFactory;
import jdk.nashorn.internal.runtime.linker.PrimitiveLookup;

/**
 * ECMA 15.7 Number Objects.
 *
 */
@ScriptClass("Number")
public final class NativeNumber extends ScriptObject {

    static final MethodHandle WRAPFILTER = findWrapFilter();

    /** ECMA 15.7.3.2 largest positive finite value */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT, where = Where.CONSTRUCTOR)
    public static final double MAX_VALUE = Double.MAX_VALUE;

    /** ECMA 15.7.3.3 smallest positive finite value */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT, where = Where.CONSTRUCTOR)
    public static final double MIN_VALUE = Double.MIN_VALUE;

    /** ECMA 15.7.3.4 NaN */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT, where = Where.CONSTRUCTOR)
    public static final double NaN = Double.NaN;

    /** ECMA 15.7.3.5 negative infinity */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT, where = Where.CONSTRUCTOR)
    public static final double NEGATIVE_INFINITY = Double.NEGATIVE_INFINITY;

    /** ECMA 15.7.3.5 positive infinity */
    @Property(attributes = Attribute.NON_ENUMERABLE_CONSTANT, where = Where.CONSTRUCTOR)
    public static final double POSITIVE_INFINITY = Double.POSITIVE_INFINITY;

    private final double  value;
    private final boolean isInt;
    private final boolean isLong;

    NativeNumber(final double value) {
        this(value, Global.instance().getNumberPrototype());
    }

    private NativeNumber(final double value, final ScriptObject proto) {
        this.value = value;
        this.isInt  = isRepresentableAsInt(value);
        this.isLong = isRepresentableAsLong(value);
        this.setProto(proto);
    }

    @Override
    public String safeToString() {
        return "[Number " + toString() + "]";
    }

    @Override
    public String toString() {
        return Double.toString(getValue());
    }

    /**
     * Get the value of this Number
     * @return a {@code double} representing the Number value
     */
    public double getValue() {
        return doubleValue();
    }

    /**
     * Get the value of this Number
     * @return a {@code double} representing the Number value
     */
    public double doubleValue() {
        return value;
    }

    /**
     * Get the value of this Number as a {@code int}
     * @return an {@code int} representing the Number value
     * @throws ClassCastException If number is not representable as an {@code int}
     */
    public int intValue() throws ClassCastException {
        if (isInt) {
            return (int)value;
        }
        throw new ClassCastException();
    }

    /**
     * Get the value of this Number as a {@code long}
     * @return a {@code long} representing the Number value
     * @throws ClassCastException If number is not representable as an {@code long}
     */
    public long longValue() throws ClassCastException {
        if (isLong) {
            return (long)value;
        }
        throw new ClassCastException();
    }

    @Override
    public String getClassName() {
        return "Number";
    }

    /**
     * ECMA 15.7.2 - The Number constructor
     *
     * @param newObj is this Number instantiated with the new operator
     * @param self   self reference
     * @param args   value of number
     * @return the Number instance (internally represented as a {@code NativeNumber})
     */
    @Constructor(arity = 1)
    public static Object constructor(final boolean newObj, final Object self, final Object... args) {
        final double num = (args.length > 0) ? JSType.toNumber(args[0]) : 0.0;

        if (newObj) {
            final ScriptObject proto =
                (self instanceof ScriptObject) ?
                    ((ScriptObject)self).getProto() :
                    Global.instance().getNumberPrototype();

            return new NativeNumber(num, proto);
        }

        return num;
    }

    /**
     * ECMA 15.7.4.5 Number.prototype.toFixed (fractionDigits)
     *
     * @param self           self reference
     * @param fractionDigits how many digits should be after the decimal point, 0 if undefined
     *
     * @return number in decimal fixed point notation
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toFixed(final Object self, final Object fractionDigits) {
        final int f = JSType.toInteger(fractionDigits);
        if (f < 0 || f > 20) {
            throw rangeError("invalid.fraction.digits", "toFixed");
        }

        final double x = getNumberValue(self);
        if (Double.isNaN(x)) {
            return "NaN";
        }

        if (Math.abs(x) >= 1e21) {
            return JSType.toString(x);
        }

        final NumberFormat format = NumberFormat.getNumberInstance(Locale.US);
        format.setMinimumFractionDigits(f);
        format.setMaximumFractionDigits(f);
        format.setGroupingUsed(false);

        return format.format(x);
    }

    /**
     * ECMA 15.7.4.6 Number.prototype.toExponential (fractionDigits)
     *
     * @param self           self reference
     * @param fractionDigits how many digital should be after the significand's decimal point. If undefined, use as many as necessary to uniquely specify number.
     *
     * @return number in decimal exponential notation
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toExponential(final Object self, final Object fractionDigits) {
        final double  x         = getNumberValue(self);
        final boolean trimZeros = fractionDigits == UNDEFINED;
        final int     f         = trimZeros ? 16 : JSType.toInteger(fractionDigits);

        if (Double.isNaN(x)) {
            return "NaN";
        } else if (Double.isInfinite(x)) {
            return x > 0? "Infinity" : "-Infinity";
        }

        if (fractionDigits != UNDEFINED && (f < 0 || f > 20)) {
            throw rangeError("invalid.fraction.digits", "toExponential");
        }

        final String res = String.format(Locale.US, "%1." + f + "e", x);
        return fixExponent(res, trimZeros);
    }

    /**
     * ECMA 15.7.4.7 Number.prototype.toPrecision (precision)
     *
     * @param self      self reference
     * @param precision use {@code precision - 1} digits after the significand's decimal point or call {@link NativeDate#toString} if undefined
     *
     * @return number in decimal exponentiation notation or decimal fixed notation depending on {@code precision}
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toPrecision(final Object self, final Object precision) {
        final double x = getNumberValue(self);
        if (precision == UNDEFINED) {
            return JSType.toString(x);
        }

        final int p = JSType.toInteger(precision);
        if (Double.isNaN(x)) {
            return "NaN";
        } else if (Double.isInfinite(x)) {
            return x > 0? "Infinity" : "-Infinity";
        }

        if (p < 1 || p > 21) {
            throw rangeError("invalid.precision");
        }

        // workaround for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6469160
        if (x == 0.0 && p <= 1) {
            return "0";
        }

        return fixExponent(String.format(Locale.US, "%." + p + "g", x), false);
    }

    /**
     * ECMA 15.7.4.2 Number.prototype.toString ( [ radix ] )
     *
     * @param self  self reference
     * @param radix radix to use for string conversion
     * @return string representation of this Number in the given radix
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toString(final Object self, final Object radix) {
        if (radix != UNDEFINED) {
            final int intRadix = JSType.toInteger(radix);
            if (intRadix != 10) {
                if (intRadix < 2 || intRadix > 36) {
                    throw rangeError("invalid.radix");
                }
                return JSType.toString(getNumberValue(self), intRadix);
            }
        }

        return JSType.toString(getNumberValue(self));
    }

    /**
     * ECMA 15.7.4.3 Number.prototype.toLocaleString()
     *
     * @param self self reference
     * @return localized string for this Number
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toLocaleString(final Object self) {
        return JSType.toString(getNumberValue(self));
    }


    /**
     * ECMA 15.7.4.4 Number.prototype.valueOf ( )
     *
     * @param self self reference
     * @return boxed number value for this Number
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object valueOf(final Object self) {
        return getNumberValue(self);
    }

    /**
     * Lookup the appropriate method for an invoke dynamic call.
     * @param request  The link request
     * @param receiver receiver of call
     * @return Link to be invoked at call site.
     */
    public static GuardedInvocation lookupPrimitive(final LinkRequest request, final Object receiver) {
        return PrimitiveLookup.lookupPrimitive(request, Number.class, new NativeNumber(((Number)receiver).doubleValue()), WRAPFILTER);
    }

    @SuppressWarnings("unused")
    private static NativeNumber wrapFilter(final Object receiver) {
        return new NativeNumber(((Number)receiver).doubleValue());
    }

    private static double getNumberValue(final Object self) {
        if (self instanceof Number) {
            return ((Number)self).doubleValue();
        } else if (self instanceof NativeNumber) {
            return ((NativeNumber)self).getValue();
        } else if (self != null && self == Global.instance().getNumberPrototype()) {
            return 0.0;
        } else {
            throw typeError("not.a.number", ScriptRuntime.safeToString(self));
        }
    }

    // Exponent of Java "e" or "E" formatter is always 2 digits and zero
    // padded if needed (e+01, e+00, e+12 etc.) JS expects exponent to contain
    // exact number of digits e+1, e+0, e+12 etc. Fix the exponent here.
    //
    // Additionally, if trimZeros is true, this cuts trailing zeros in the
    // fraction part for calls to toExponential() with undefined fractionDigits
    // argument.
    private static String fixExponent(final String str, final boolean trimZeros) {
        final int index = str.indexOf('e');
        if (index < 1) {
            // no exponent, do nothing..
            return str;
        }

        // check if character after e+ or e- is 0
        final int expPadding = str.charAt(index + 2) == '0' ? 3 : 2;
        // check if there are any trailing zeroes we should remove

        int fractionOffset = index;
        if (trimZeros) {
            assert fractionOffset > 0;
            char c = str.charAt(fractionOffset - 1);
            while (fractionOffset > 1 && (c == '0' || c == '.')) {
                c = str.charAt(--fractionOffset - 1);
            }

        }
        // if anything needs to be done compose a new string
        if (fractionOffset < index || expPadding == 3) {
            return str.substring(0, fractionOffset)
                    + str.substring(index, index + 2)
                    + str.substring(index + expPadding);
        }
        return str;
    }

    private static MethodHandle findWrapFilter() {
        try {
            return MethodHandles.lookup().findStatic(NativeNumber.class, "wrapFilter", MH.type(NativeNumber.class, Object.class));
        } catch (final NoSuchMethodException | IllegalAccessException e) {
            throw new MethodHandleFactory.LookupException(e);
        }
    }
}