All files / src/evaluation-context java-number-methods.ts

100% Statements 28/28
100% Branches 31/31
100% Functions 4/4
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                  24x     18x 15x   3x 2x   1x         16x         12x                       56x   6x   3x   1x   6x       6x           4x   6x   7x 7x 2x   5x 2x 2x 2x   3x     4x 4x 4x     13x      
/**
 * Numeric wrapper method invocation.
 *
 * SpEL resolves a method call on a number against the Java wrapper classes —
 * `Integer`, `Long`, `Double`, `Float`, `Byte`, `Short` — and a
 * `java.math.BigInteger` when the value came from one. JavaScript's `Number`
 * offers a different set under different names, so a number is resolved against
 * this table only, for the same reason a string is resolved against
 * `java.lang.String` only. A shared receiver would let a JavaScript method answer
 * for a Java name.
 *
 * That is why `toFixed` and `toExponential` no longer resolve. Neither is a
 * member of any Java type, so Spring raises method-not-found; they were callable
 * only because JavaScript's `Number` happened to provide them. The Java ways to
 * format a number are `T(String).format('%.2f', n)`, which the type catalogue
 * makes available, or `T(Math).round(n * 100) / 100` for a rounded value.
 *
 * The reverse gap is closed here as well: `intValue`, `doubleValue` and their
 * siblings exist in Java and previously had no implementation.
 */
import { TypedValue } from '../typed-value.js';
import { SpelEvaluationException } from '../error/spel-evaluation-exception.js';
import { SpelMessage } from '../error/spel-message.js';
 
/** Position used when the caller does not know it; the AST node fills it in. */
const UNKNOWN_POSITION = -1;
 
function requireNumber(value: unknown, method: string, position: number): number {
  if (typeof value === 'number') {
    return value;
  }
  if (typeof value === 'bigint') {
    return Number(value);
  }
  throw new SpelEvaluationException(position, SpelMessage.TYPE_CONVERSION_ERROR, method, 'number');
}
 
/** Truncate toward zero, keeping the value exact when it is already a BigInt. */
function toTruncatedBigInt(value: number | bigint): bigint {
  return typeof value === 'bigint' ? value : BigInt(Math.trunc(value));
}
 
/** Java's narrowing conversions keep the low bits, so a byte is `value & 0xFF`. */
function narrow(value: number | bigint, bits: number): number {
  return Number(BigInt.asIntN(bits, toTruncatedBigInt(value)));
}
 
/**
 * Invoke a Java wrapper method, or return null when the name is not one.
 */
export function invokeNumberMethod(
  target: number | bigint,
  name: string,
  args: readonly unknown[],
  position: number = UNKNOWN_POSITION,
): TypedValue | null {
  switch (name) {
    case 'toString':
      return new TypedValue(String(target));
    case 'byteValue':
      return new TypedValue(narrow(target, 8), null, 'int');
    case 'shortValue':
      return new TypedValue(narrow(target, 16), null, 'int');
    case 'intValue':
      return new TypedValue(narrow(target, 32), null, 'int');
    case 'longValue':
      // A JavaScript number's integral part fits in 64 bits, so truncation is
      // exact; a BigInt takes its low 64 bits, as Java does.
      return new TypedValue(
        typeof target === 'bigint' ? narrow(target, 64) : Math.trunc(target),
        null,
        'long',
      );
    case 'floatValue':
      return new TypedValue(Math.fround(requireNumber(target, name, position)), null, 'float');
    case 'doubleValue':
      return new TypedValue(requireNumber(target, name, position), null, 'double');
    case 'equals': {
      const other = args[0];
      if (typeof other !== 'number' && typeof other !== 'bigint') {
        return new TypedValue(false);
      }
      if (typeof target === 'bigint' || typeof other === 'bigint') {
        const left = toTruncatedBigInt(target);
        const right = toTruncatedBigInt(other);
        return new TypedValue(left === right);
      }
      return new TypedValue(target === other);
    }
    case 'compareTo': {
      const other = requireNumber(args[0], name, position);
      const self = requireNumber(target, name, position);
      return new TypedValue(self < other ? -1 : self > other ? 1 : 0);
    }
    default:
      return null;
  }
}