scratch-vm/src/util/cast.js

253 lines
8.3 KiB
JavaScript
Raw Normal View History

2017-04-17 15:10:04 -04:00
const Color = require('../util/color');
/**
* Store and possibly polyfill Number.isNaN. Number.isNaN can save time over
* self.isNaN by not coercing its input. We need to polyfill it to support
* Internet Explorer.
* @const
*/
const _NumberIsNaN = Number.isNaN || isNaN;
/**
* @fileoverview
* Utilities for casting and comparing Scratch data-types.
* Scratch behaves slightly differently from JavaScript in many respects,
* and these differences should be encapsulated below.
* For example, in Scratch, add(1, join("hello", world")) -> 1.
* This is because "hello world" is cast to 0.
* In JavaScript, 1 + Number("hello" + "world") would give you NaN.
* Use when coercing a value before computation.
*/
2017-04-17 19:42:48 -04:00
class Cast {
/**
* Scratch cast to number.
* Treats NaN as 0.
* In Scratch 2.0, this is captured by `interp.numArg.`
* @param {*} value Value to cast to number.
* @return {number} The Scratch-casted number value.
*/
static toNumber (value) {
// If value is already a number we don't need to coerce it with
// Number().
if (typeof value === 'number') {
// Scratch treats NaN as 0, when needed as a number.
// E.g., 0 + NaN -> 0.
if (_NumberIsNaN(value)) {
return 0;
}
return value;
}
let n;
if (Cast.isInfinity(value)) {
n = Infinity;
} else if (Cast.isNegativeInfinity(value)) {
n = -Infinity;
} else {
n = Number(value);
}
if (_NumberIsNaN(n)) {
2017-04-17 19:42:48 -04:00
// Scratch treats NaN as 0, when needed as a number.
// E.g., 0 + NaN -> 0.
return 0;
}
return n;
}
2017-04-17 19:42:48 -04:00
/**
* Scratch cast to boolean.
* In Scratch 2.0, this is captured by `interp.boolArg.`
* Treats some string values differently from JavaScript.
* @param {*} value Value to cast to boolean.
* @return {boolean} The Scratch-casted boolean value.
*/
static toBoolean (value) {
// Already a boolean?
if (typeof value === 'boolean') {
return value;
}
2017-04-17 19:42:48 -04:00
if (typeof value === 'string') {
// These specific strings are treated as false in Scratch.
if ((value === '') ||
(value === '0') ||
(value.toLowerCase() === 'false')) {
return false;
}
// All other strings treated as true.
return true;
}
// Coerce other values and numbers.
return Boolean(value);
}
2017-04-17 19:42:48 -04:00
/**
* Scratch cast to string.
* @param {*} value Value to cast to string.
* @return {string} The Scratch-casted string value.
*/
static toString (value) {
return String(value);
}
2017-04-17 19:42:48 -04:00
/**
* Cast any Scratch argument to an RGB color array to be used for the renderer.
* @param {*} value Value to convert to RGB color array.
* @return {Array.<number>} [r,g,b], values between 0-255.
*/
static toRgbColorList (value) {
const color = Cast.toRgbColorObject(value);
return [color.r, color.g, color.b];
}
2017-04-17 19:42:48 -04:00
/**
* Cast any Scratch argument to an RGB color object to be used for the renderer.
* @param {*} value Value to convert to RGB color object.
* @return {RGBOject} [r,g,b], values between 0-255.
*/
static toRgbColorObject (value) {
let color;
if (typeof value === 'string' && value.substring(0, 1) === '#') {
color = Color.hexToRgb(value);
} else {
color = Color.decimalToRgb(Cast.toNumber(value));
}
return color;
}
2017-04-17 19:42:48 -04:00
/**
* Determine if a Scratch argument is a white space string (or null / empty).
* @param {*} val value to check.
* @return {boolean} True if the argument is all white spaces or null / empty.
*/
static isWhiteSpace (val) {
2017-08-26 13:14:26 -04:00
return val === null || (typeof val === 'string' && val.trim().length === 0);
}
2017-04-17 19:42:48 -04:00
/**
* Determine if a Scratch argument is infinity.
* @param {*} val value to check.
* @return {boolean} True if the argument is any capitalization of infinity.
*/
static isInfinity (val) {
return val === Infinity || (typeof val === 'string' && val.toLowerCase() === 'infinity');
}
/**
* Determine if a Scratch argument is negative infinity.
* @param {*} val value to check.
* @return {boolean} True if the argument is a '-' folled by any capitalization of infinity.
*/
static isNegativeInfinity (val) {
return val === -Infinity || (typeof val === 'string' && val.toLowerCase() === '-infinity');
}
2017-04-17 19:42:48 -04:00
/**
* Compare two values, using Scratch cast, case-insensitive string compare, etc.
* In Scratch 2.0, this is captured by `interp.compare.`
* @param {*} v1 First value to compare.
* @param {*} v2 Second value to compare.
* @returns {number} Negative number if v1 < v2; 0 if equal; positive otherwise.
*/
static compare (v1, v2) {
// If Cast.toNumber returns '0', then the value might be NaN. If the value is NaN,
// this comparison algorithm needs to know.
let n1 = Cast.toNumber(v1) || Number(v1);
let n2 = Cast.toNumber(v2) || Number(v2);
2017-04-17 19:42:48 -04:00
if (n1 === 0 && Cast.isWhiteSpace(v1)) {
n1 = NaN;
} else if (n2 === 0 && Cast.isWhiteSpace(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
// At least one argument can't be converted to a number.
// Scratch compares strings as case insensitive.
const s1 = String(v1).toLowerCase();
const s2 = String(v2).toLowerCase();
2018-09-17 17:07:26 -04:00
if (s1 < s2) {
return -1;
} else if (s1 > s2) {
2018-09-17 17:07:26 -04:00
return 1;
}
2018-09-17 17:07:26 -04:00
return 0;
2017-04-17 19:42:48 -04:00
}
2017-08-26 13:07:47 -04:00
// Compare as numbers.
const r = n1 - n2;
if (isNaN(r)) {
if (n1 === Infinity && n2 === Infinity) {
return 0;
}
if (n1 === -Infinity && n2 === -Infinity) {
return 0;
}
}
return r;
}
2017-04-17 19:42:48 -04:00
/**
* Determine if a Scratch argument number represents a round integer.
* @param {*} val Value to check.
* @return {boolean} True if number looks like an integer.
*/
static isInt (val) {
// Values that are already numbers.
if (typeof val === 'number') {
if (isNaN(val)) { // NaN is considered an integer.
return true;
}
// True if it's "round" (e.g., 2.0 and 2).
return val === parseInt(val, 10);
} else if (typeof val === 'boolean') {
// `True` and `false` always represent integer after Scratch cast.
return true;
2017-04-17 19:42:48 -04:00
} else if (typeof val === 'string') {
// If it contains a decimal point, don't consider it an int.
return val.indexOf('.') < 0;
}
2017-04-17 19:42:48 -04:00
return false;
}
2017-04-17 19:42:48 -04:00
static get LIST_INVALID () {
return 'INVALID';
}
static get LIST_ALL () {
return 'ALL';
}
/**
* Compute a 1-based index into a list, based on a Scratch argument.
* Two special cases may be returned:
* LIST_ALL: if the block is referring to all of the items in the list.
* LIST_INVALID: if the index was invalid in any way.
* @param {*} index Scratch arg, including 1-based numbers or special cases.
* @param {number} length Length of the list.
* @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
*/
static toListIndex (index, length) {
if (typeof index !== 'number') {
if (index === 'all') {
return Cast.LIST_ALL;
}
2017-04-17 19:42:48 -04:00
if (index === 'last') {
if (length > 0) {
return length;
}
return Cast.LIST_INVALID;
} else if (index === 'random' || index === 'any') {
if (length > 0) {
return 1 + Math.floor(Math.random() * length);
}
return Cast.LIST_INVALID;
}
2017-04-17 19:42:48 -04:00
}
index = Math.floor(Cast.toNumber(index));
if (index < 1 || index > length) {
return Cast.LIST_INVALID;
}
2017-04-17 19:42:48 -04:00
return index;
}
2017-04-17 19:42:48 -04:00
}
module.exports = Cast;