Files
blockly/generators/php/math.ts
Christopher Allen 7bda30924a refactor(generators): Migrate PHP generators to TypeScript (#7647)
* refactor(generators): Migrate dart_generator.js to TypeScript

* refactor(generators): Simplify getAdjusted

  Slightly simplify the implementation of getAdjusted, in part to
  make it more readable.  Also improve its JSDoc comment.

* refactor(generators): Migrate generators/php/* to TypeScript

  First pass doing very mechanistic migration, not attempting to fix
  all the resulting type errors.

* fix(generators): Fix type errors in generator functions

  This consists almost entirely of adding casts, so the code output
  by tsc should be as similar as possible to the pre-migration .js
  source files.

* fix(generators): Fix more minor inconsistencies in JS and Python

  The migration of the JavaScript and Python generators
  inadvertently introduced some inconsistencies in the code,
  e.g. putting executable code before the initial comment line that
  most generator functions begin with.  This fixes another instance
  of this (but n.b. that these inline comments should have been
  JSDocs and a task has been added to #7600 to convert them).

  Additionally, I noticed while doing the PHP migration that
  ORDER_OVERRIDES was not typed as specifically as it could be,
  in previous migrations, so this is fixed here (along with the
  formatting of the associated JSDoc, which can fit on one line
  now.)

* refactor(generators): Migrate generators/php.js to TypeScript

  The way the generator functions are added to
  phpGenerator.forBlock has been modified so that incorrect
  generator function signatures will cause tsc to generate a type
  error.

* chore(generator): Format

  One block protected with // prettier-ignore to preserve careful
  comment formatting.

  Where there are repeated concatenations prettier has made a pretty
  mess of things, but the correct fix is probably to use template
  literals instead (rather than just locally disabling prettier).
  This is one of the items in the to-do list in #7600.

* docs(generators): @fileoverview -> @file

  With an update to the wording for generators/php.ts.

* fix(generators): Fixes for PR #7647.

  - Don't declare unused wherePascalCase dictionary.
  - Don't allow null in OPERATOR dictionary when not needed.
  - Fix return type (and documentation thereof) of getAdjusted.
2023-11-28 16:59:19 +00:00

409 lines
12 KiB
TypeScript

/**
* @license
* Copyright 2015 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file Generating PHP for math blocks.
*/
// Former goog.module ID: Blockly.PHP.math
import type {Block} from '../../core/block.js';
import {Order} from './php_generator.js';
import type {PhpGenerator} from './php_generator.js';
export function math_number(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Numeric value.
let number = Number(block.getFieldValue('NUM'));
if (number === Infinity) {
return ['INF', Order.ATOMIC];
} else if (number === -Infinity) {
return ['-INF', Order.UNARY_NEGATION];
}
return [String(number), number >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION];
}
export function math_arithmetic(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Basic arithmetic operators, and power.
const OPERATORS: Record<string, [string, Order]> = {
'ADD': [' + ', Order.ADDITION],
'MINUS': [' - ', Order.SUBTRACTION],
'MULTIPLY': [' * ', Order.MULTIPLICATION],
'DIVIDE': [' / ', Order.DIVISION],
'POWER': [' ** ', Order.POWER],
};
type OperatorOption = keyof typeof OPERATORS;
const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption];
const operator = tuple[0];
const order = tuple[1];
const argument0 = generator.valueToCode(block, 'A', order) || '0';
const argument1 = generator.valueToCode(block, 'B', order) || '0';
const code = argument0 + operator + argument1;
return [code, order];
}
export function math_single(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Math operators with single operand.
const operator = block.getFieldValue('OP');
let code;
let arg;
if (operator === 'NEG') {
// Negation is a special case given its different operator precedence.
arg = generator.valueToCode(block, 'NUM', Order.UNARY_NEGATION) || '0';
if (arg[0] === '-') {
// --3 is not legal in JS.
arg = ' ' + arg;
}
code = '-' + arg;
return [code, Order.UNARY_NEGATION];
}
if (operator === 'SIN' || operator === 'COS' || operator === 'TAN') {
arg = generator.valueToCode(block, 'NUM', Order.DIVISION) || '0';
} else {
arg = generator.valueToCode(block, 'NUM', Order.NONE) || '0';
}
// First, handle cases which generate values that don't need parentheses
// wrapping the code.
switch (operator) {
case 'ABS':
code = 'abs(' + arg + ')';
break;
case 'ROOT':
code = 'sqrt(' + arg + ')';
break;
case 'LN':
code = 'log(' + arg + ')';
break;
case 'EXP':
code = 'exp(' + arg + ')';
break;
case 'POW10':
code = 'pow(10,' + arg + ')';
break;
case 'ROUND':
code = 'round(' + arg + ')';
break;
case 'ROUNDUP':
code = 'ceil(' + arg + ')';
break;
case 'ROUNDDOWN':
code = 'floor(' + arg + ')';
break;
case 'SIN':
code = 'sin(' + arg + ' / 180 * pi())';
break;
case 'COS':
code = 'cos(' + arg + ' / 180 * pi())';
break;
case 'TAN':
code = 'tan(' + arg + ' / 180 * pi())';
break;
}
if (code) {
return [code, Order.FUNCTION_CALL];
}
// Second, handle cases which generate values that may need parentheses
// wrapping the code.
switch (operator) {
case 'LOG10':
code = 'log(' + arg + ') / log(10)';
break;
case 'ASIN':
code = 'asin(' + arg + ') / pi() * 180';
break;
case 'ACOS':
code = 'acos(' + arg + ') / pi() * 180';
break;
case 'ATAN':
code = 'atan(' + arg + ') / pi() * 180';
break;
default:
throw Error('Unknown math operator: ' + operator);
}
return [code, Order.DIVISION];
}
export function math_constant(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY.
const CONSTANTS: Record<string, [string, Order]> = {
'PI': ['M_PI', Order.ATOMIC],
'E': ['M_E', Order.ATOMIC],
'GOLDEN_RATIO': ['(1 + sqrt(5)) / 2', Order.DIVISION],
'SQRT2': ['M_SQRT2', Order.ATOMIC],
'SQRT1_2': ['M_SQRT1_2', Order.ATOMIC],
'INFINITY': ['INF', Order.ATOMIC],
};
type ConstantOption = keyof typeof CONSTANTS;
return CONSTANTS[block.getFieldValue('CONSTANT') as ConstantOption];
}
export function math_number_property(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Check if a number is even, odd, prime, whole, positive, or negative
// or if it is divisible by certain number. Returns true or false.
const PROPERTIES: Record<
string,
[string, string, Order, Order] | [null, null, Order, Order]
> = {
'EVEN': ['', ' % 2 == 0', Order.MODULUS, Order.EQUALITY],
'ODD': ['', ' % 2 == 1', Order.MODULUS, Order.EQUALITY],
'WHOLE': ['is_int(', ')', Order.NONE, Order.FUNCTION_CALL],
'POSITIVE': ['', ' > 0', Order.RELATIONAL, Order.RELATIONAL],
'NEGATIVE': ['', ' < 0', Order.RELATIONAL, Order.RELATIONAL],
'DIVISIBLE_BY': [null, null, Order.MODULUS, Order.EQUALITY],
'PRIME': [null, null, Order.NONE, Order.FUNCTION_CALL],
};
type PropertyOption = keyof typeof PROPERTIES;
const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption;
const [prefix, suffix, inputOrder, outputOrder] =
PROPERTIES[dropdownProperty];
const numberToCheck =
generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0';
let code;
if (dropdownProperty === 'PRIME') {
// Prime is a special case as it is not a one-liner test.
const functionName = generator.provideFunction_(
'math_isPrime',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($n) {
// https://en.wikipedia.org/wiki/Primality_test#Naive_methods
if ($n == 2 || $n == 3) {
return true;
}
// False if n is NaN, negative, is 1, or not whole.
// And false if n is divisible by 2 or 3.
if (!is_numeric($n) || $n <= 1 || $n % 1 != 0 || $n % 2 == 0 || $n % 3 == 0) {
return false;
}
// Check all the numbers of form 6k +/- 1, up to sqrt(n).
for ($x = 6; $x <= sqrt($n) + 1; $x += 6) {
if ($n % ($x - 1) == 0 || $n % ($x + 1) == 0) {
return false;
}
}
return true;
}
`,
);
code = functionName + '(' + numberToCheck + ')';
} else if (dropdownProperty === 'DIVISIBLE_BY') {
const divisor =
generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0';
if (divisor === '0') {
return ['false', Order.ATOMIC];
}
code = numberToCheck + ' % ' + divisor + ' == 0';
} else {
code = prefix + numberToCheck + suffix;
}
return [code, outputOrder];
}
export function math_change(block: Block, generator: PhpGenerator) {
// Add to a variable in place.
const argument0 =
generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0';
const varName = generator.getVariableName(block.getFieldValue('VAR'));
return varName + ' += ' + argument0 + ';\n';
}
// Rounding functions have a single operand.
export const math_round = math_single;
// Trigonometry functions have a single operand.
export const math_trig = math_single;
export function math_on_list(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Math functions for lists.
const func = block.getFieldValue('OP');
let list;
let code;
switch (func) {
case 'SUM':
list =
generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()';
code = 'array_sum(' + list + ')';
break;
case 'MIN':
list =
generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()';
code = 'min(' + list + ')';
break;
case 'MAX':
list =
generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()';
code = 'max(' + list + ')';
break;
case 'AVERAGE': {
const functionName = generator.provideFunction_(
'math_mean',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($myList) {
return array_sum($myList) / count($myList);
}
`,
);
list = generator.valueToCode(block, 'LIST', Order.NONE) || 'array()';
code = functionName + '(' + list + ')';
break;
}
case 'MEDIAN': {
const functionName = generator.provideFunction_(
'math_median',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($arr) {
sort($arr,SORT_NUMERIC);
return (count($arr) % 2) ? $arr[floor(count($arr) / 2)] :
($arr[floor(count($arr) / 2)] + $arr[floor(count($arr) / 2) - 1]) / 2;
}
`,
);
list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]';
code = functionName + '(' + list + ')';
break;
}
case 'MODE': {
// As a list of numbers can contain more than one mode,
// the returned result is provided as an array.
// Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1].
const functionName = generator.provideFunction_(
'math_modes',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($values) {
if (empty($values)) return array();
$counts = array_count_values($values);
arsort($counts); // Sort counts in descending order
$modes = array_keys($counts, current($counts), true);
return $modes;
}
`,
);
list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]';
code = functionName + '(' + list + ')';
break;
}
case 'STD_DEV': {
const functionName = generator.provideFunction_(
'math_standard_deviation',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($numbers) {
$n = count($numbers);
if (!$n) return null;
$mean = array_sum($numbers) / count($numbers);
foreach($numbers as $key => $num) $devs[$key] = pow($num - $mean, 2);
return sqrt(array_sum($devs) / (count($devs) - 1));
}
`,
);
list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]';
code = functionName + '(' + list + ')';
break;
}
case 'RANDOM': {
const functionName = generator.provideFunction_(
'math_random_list',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list) {
$x = rand(0, count($list)-1);
return $list[$x];
}
`,
);
list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]';
code = functionName + '(' + list + ')';
break;
}
default:
throw Error('Unknown operator: ' + func);
}
return [code, Order.FUNCTION_CALL];
}
export function math_modulo(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Remainder computation.
const argument0 =
generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0';
const argument1 =
generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0';
const code = argument0 + ' % ' + argument1;
return [code, Order.MODULUS];
}
export function math_constrain(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Constrain a number between two limits.
const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0';
const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0';
const argument2 =
generator.valueToCode(block, 'HIGH', Order.NONE) || 'Infinity';
const code =
'min(max(' + argument0 + ', ' + argument1 + '), ' + argument2 + ')';
return [code, Order.FUNCTION_CALL];
}
export function math_random_int(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Random integer between [X] and [Y].
const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0';
const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0';
const functionName = generator.provideFunction_(
'math_random_int',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}($a, $b) {
if ($a > $b) {
return rand($b, $a);
}
return rand($a, $b);
}
`,
);
const code = functionName + '(' + argument0 + ', ' + argument1 + ')';
return [code, Order.FUNCTION_CALL];
}
export function math_random_float(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Random fraction between 0 and 1.
return ['(float)rand()/(float)getrandmax()', Order.FUNCTION_CALL];
}
export function math_atan2(
block: Block,
generator: PhpGenerator,
): [string, Order] {
// Arctangent of point (X, Y) in degrees from -180 to 180.
const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0';
const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0';
return [
'atan2(' + argument1 + ', ' + argument0 + ') / pi() * 180',
Order.DIVISION,
];
}