You are here

units.module in Units of Measurement 7.2

Same filename and directory in other branches
  1. 6 units.module
  2. 7 units.module

Provide API for managing and converting units of measurement.

File

units.module
View source
<?php

/**
 * @file
 * Provide API for managing and converting units of measurement.
 */

/**
 * Denote placeholder for quantity to use in decomposition of non-linear units.
 */
define('UNITS_QUANTITY', 'value');

/**
 * Denote constant token type.
 *
 * @var int
 */
define('UNITS_TOKEN_TYPE_CONSTANT', 0);

/**
 * Denote unit token type.
 *
 * @var int
 */
define('UNITS_TOKEN_TYPE_UNIT', 1);

/**
 * Denote operator token type.
 *
 * @var int
 */
define('UNITS_TOKEN_TYPE_OPERATOR', 2);

/**
 * Implements hook_entity_info().
 */
function units_entity_info() {
  $entity_info = array();
  $entity_info['units_unit'] = array(
    'label' => t('Unit measurement'),
    'entity class' => 'UnitsEntity',
    'controller class' => 'EntityAPIControllerExportable',
    'base table' => 'units_unit',
    'fieldable' => TRUE,
    'exportable' => TRUE,
    'entity keys' => array(
      'id' => 'umid',
      'bundle' => 'measure',
      'label' => 'label',
      'name' => 'machine_name',
    ),
    'bundles' => array(),
    'bundle keys' => array(
      'bundle' => 'measure',
    ),
    'module' => 'units',
    'access callback' => 'units_entity_access',
  );

  // We can't use here entity_load functions, nor EntityFieldQuery because
  // entity info is not exposed to core yet.
  $measures = db_select('units_measure', 'u_m')
    ->fields('u_m', array(
    'measure',
    'label',
  ))
    ->execute()
    ->fetchAllAssoc('measure');
  foreach ($measures as $measure) {
    $entity_info['units_unit']['bundles'][$measure->measure] = array(
      'label' => $measure->label,
    );
  }
  $entity_info['units_measure'] = array(
    'label' => t('Measure'),
    'entity class' => 'Entity',
    'controller class' => 'EntityAPIControllerExportable',
    'base table' => 'units_measure',
    'fieldable' => FALSE,
    'exportable' => TRUE,
    'bundle of' => 'units_unit',
    'entity keys' => array(
      'id' => 'mid',
      'label' => 'label',
      'name' => 'measure',
    ),
    'module' => 'units',
    'access callback' => 'units_entity_access',
  );
  return $entity_info;
}

/**
 * Implements hook_ctools_plugin_type().
 */
function units_ctools_plugin_type() {
  $plugins = array();
  $plugins['operator'] = array(
    'defaults' => array(
      'title' => NULL,
      'description' => NULL,
      'sign' => NULL,
      'transparent operand1' => 0,
      'transparent operand2' => 0,
      'precedence' => NULL,
      'dimension check' => TRUE,
      'evaluate callback' => NULL,
      'dimension callback' => NULL,
      'split quantity callback' => NULL,
      'isolate operand callback' => NULL,
      'sql evaluate fragment' => NULL,
      'operator class' => 'UnitsMathematicalOperatorLinear',
    ),
  );
  return $plugins;
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function units_ctools_plugin_directory($owner, $plugin_type) {
  switch ($owner) {
    case 'units':
      switch ($plugin_type) {
        case 'operator':
          return 'plugins/' . $plugin_type;
      }
      break;
  }
}

/**
 * Implements hook_element_info().
 */
function units_element_info() {
  return array(
    'units_mathematical_expression' => array(
      '#input' => TRUE,
      '#process' => array(
        'units_mathematical_expression_element_process',
      ),
      '#element_validate' => array(
        'units_mathematical_expression_element_validate',
      ),
      // In what format to represent the submitted mathematical expression.
      // Allowed values are:
      // - object: to represent as an instance of
      //   UnitsMathematicalExpressionWrapper
      // - infix: to represent as a string in infix notation
      // - postfix: to represent as a string in postfix notation
      '#value_format' => 'object',
      // If the form element should accept only mathematical expressions of a
      // specific dimension, put the allowed dimension here.
      '#allowed_dimension' => NULL,
    ),
  );
}

/**
 * Access callback for entity types 'units_measure' and 'units_unit'.
 *
 * @param string $op
 *   The operation being performed. One of 'view', 'update', 'create' or
 *   'delete'
 * @param object $entity
 *   Entity object on which the operation is requested to be performed
 * @param object $account
 *   Fully loaded user object of the account who requests to perform the
 *   operation
 * @param string $entity_type
 *   Entity type on which the operation is requested to be performed
 *
 * @return bool
 *   Whether access has been granted
 */
function units_entity_access($op, $entity, $account, $entity_type) {

  // There is no reason why we would limit access to 'units_measure' or
  // 'units_unit' entities.
  return TRUE;
}

/**
 * Implements hook_ENTITY_TYPE_load().
 */
function units_units_unit_load($entities) {

  // TODO: it should be replace with a batch loading all involved mathematical
  // expressions in a single SQL SELECT.
  foreach ($entities as $entity) {
    $entity->decomposition = $entity->decomposition_mathematical_expression_id ? units_mathematical_expression_load($entity->decomposition_mathematical_expression_id) : NULL;
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function units_units_unit_presave($entity) {
  if (isset($entity->original->decomposition_mathematical_expression_id)) {
    units_mathematical_expression_delete($entity->original->decomposition_mathematical_expression_id);
  }
  if (is_object($entity->decomposition)) {
    $entity->decomposition
      ->save();
    $entity->decomposition_mathematical_expression_id = $entity->decomposition
      ->getMathematicalExpressionId();
  }
  else {
    $entity->decomposition_mathematical_expression_id = 0;
  }

  // We must make sure that $entity dimension is the same as all other units
  // within its measure. It just does not make sense to have units of different
  // dimension in the same measure.
  $dimension = $entity
    ->dimension();
  foreach (units_unit_by_measure_load_multiple($entity->measure) as $unit) {
    if ($entity
      ->identifier() != $unit
      ->identifier() && !units_dimension_equal($dimension, $unit
      ->dimension())) {

      // TODO: what am I supposed to do? throw an exception? show an error message? Log it to watchdog?
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function units_units_unit_delete($entity) {
  units_mathematical_expression_delete($entity->decomposition_mathematical_expression_id);
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function units_units_measure_delete($entity) {

  // Additionally delete units defined in the measure that is being deleted.
  $ids = array();
  foreach (units_unit_by_measure_load_multiple($entity) as $unit) {
    $tmp = entity_extract_ids('units_unit', $unit);
    $ids[] = $tmp[0];
  }
  units_unit_delete_multiple($ids);
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function units_units_measure_update($entity) {
  if ($entity->measure != $entity->original->measure) {
    db_update('units_unit')
      ->condition('measure', $entity->original->measure)
      ->fields(array(
      'measure' => $entity->measure,
    ))
      ->execute();
  }
}

/**
 * Load an entity of entity type 'units_unit' by its ID.
 */
function units_unit_load($umid, $reset = FALSE) {
  $units = units_unit_load_multiple(array(
    $umid,
  ), array(), $reset);
  return reset($units);
}

/**
 * Load multiple entities of entity type 'units_unit'.
 */
function units_unit_load_multiple($umids = FALSE, $conditions = array(), $reset = FALSE) {
  return entity_load('units_unit', $umids, $conditions, $reset);
}

/**
 * Load a single entity of type 'units_unit' loading by its machine name.
 *
 * @param string $machine_name
 *   Machine name of entity to load
 *
 * @return object|bool
 *   Return fully loaded entity object if it was found, otherwise FALSE
 */
function units_unit_machine_name_load($machine_name) {
  $query = new EntityFieldQuery();
  $result = $query
    ->entityCondition('entity_type', 'units_unit')
    ->propertyCondition('machine_name', $machine_name)
    ->execute();
  if (isset($result['units_unit'])) {
    $keys = array_keys($result['units_unit']);
    $entity_id = array_pop($keys);
    return units_unit_load($entity_id);
  }

  // No entity was found.
  return FALSE;
}

/**
 * Load all units of the supplied measure.
 *
 * @param mixed $measure
 *   Either ID of the measure,
 *   or machine-readable name of the measure
 *   or fully loaded 'units_measure' entity object
 *
 * @return array
 *   Array of fully loaded 'units_unit' entity objects that belong to the
 *   supplied $measure
 */
function units_unit_by_measure_load_multiple($measure) {

  // Trying to load entity object of $measure, if we were not supplied with one.
  if (is_numeric($measure)) {
    $measure = units_measure_load($measure);
  }
  elseif (!is_object($measure)) {
    $measure = units_measure_machine_name_load($measure);
  }
  if (!is_object($measure)) {

    // Probably we were supplied with bad parameter $measure, because at this
    // point we are already supposed to have fully loaded 'units_measure' entity
    // object.
    return array();
  }
  $bundle = field_extract_bundle('units_unit', $measure);
  $efq = new EntityFieldQuery();
  $result = $efq
    ->entityCondition('entity_type', 'units_unit')
    ->entityCondition('bundle', $bundle)
    ->execute();
  return isset($result['units_unit']) ? units_unit_load_multiple(array_keys($result['units_unit'])) : array();
}

/**
 * Save an entity of type 'units_unit'.
 */
function units_unit_save($entity) {
  entity_save('units_unit', $entity);
}

/**
 * Delete a single entity of type 'units_unit'.
 */
function units_unit_delete($entity) {
  entity_delete('units_unit', entity_id('units_unit', $entity));
}

/**
 * Delete multiple entities of type 'units_unit'.
 *
 * @param array $umids
 *   Array of entity ids to be deleted
 */
function units_unit_delete_multiple($umids) {
  entity_delete_multiple('units_unit', $umids);
}

/**
 * Load an entity of entity type 'units_measure' by its ID.
 */
function units_measure_load($mid, $reset = FALSE) {
  $measures = units_measure_load_multiple(array(
    $mid,
  ), array(), $reset);
  return reset($measures);
}

/**
 * Load multiple entities of entity type 'units_unit'.
 */
function units_measure_load_multiple($mids = FALSE, $conditions = array(), $reset = FALSE) {
  return entity_load('units_measure', $mids, $conditions, $reset);
}

/**
 * Load a single entity of type 'units_measure' loading by its machine name.
 *
 * @param string $machine_name
 *   Machine name of entity to load
 *
 * @return object|bool
 *   Return fully loaded entity object if it was found, otherwise FALSE
 */
function units_measure_machine_name_load($machine_name) {
  $query = new EntityFieldQuery();
  $result = $query
    ->entityCondition('entity_type', 'units_measure')
    ->propertyCondition('measure', $machine_name)
    ->execute();
  if (isset($result['units_measure'])) {
    $keys = array_keys($result['units_measure']);
    $entity_id = array_pop($keys);
    return units_measure_load($entity_id);
  }

  // No entity was found.
  return FALSE;
}

/**
 * Save an entity of type 'units_measure'.
 */
function units_measure_save($entity) {
  entity_save('units_measure', $entity);
}

/**
 * Delete a single entity of type 'units_measure'.
 */
function units_measure_delete($entity) {
  entity_delete('units_measure', entity_id('units_measure', $entity));
}

/**
 * Delete multiple entities of type 'units_measure'.
 *
 * @param array $mids
 *   Array of entity ids to be deleted
 */
function units_measure_delete_multiple($mids) {
  entity_delete_multiple('units_measure', $mids);
}

/**
 * Retrieve information about a cTools plugin - units operator.
 *
 * @param string $operator
 *   Name of a particular units operator plugin to return. If skipped, an array
 *   of all available units operators plugins will be returned
 *
 * @return array
 *   Depending on whether $operator is provided it will be either a single
 *   array of information about provided $operator units operator plugin or
 *   array of information on all available units operator plugins
 */
function units_get_operator($operator = NULL) {
  ctools_include('plugins');
  return ctools_get_plugins('units', 'operator', $operator);
}

/**
 * Retrieve information about a cTools plugin - units operator.
 *
 * The search is conducted by sign symbol of all operators.
 *
 * @param string $operator
 *   Sign of a particular units operator plugin to return. If skipped, an array
 *   of all available units operators will be returned
 *
 * @return array
 *   Depending on whether $operator is provided it will be either a single
 *   array of information about provided $operator units operator plugin or
 *   array of information aon all available units operator plugins, keyed by
 *   their signs
 */
function units_get_operator_by_sign($operator = NULL) {
  ctools_include('plugins');
  $return = array();
  foreach (ctools_get_plugins('units', 'operator') as $plugin) {
    $return[$plugin['sign']] = $plugin;
  }
  return isset($operator) ? $return[$operator] : $return;
}

/**
 * Check whether the 2 dimensions are equal.
 *
 * @param array $dimension1
 *   Dimension array #1
 * @param array $dimension2
 *   Dimension array #2
 *
 * @return bool
 *   Whether the 2 provided dimension arrays are equal
 */
function units_dimension_equal($dimension1, $dimension2) {
  $diff = units_dimension_subtract($dimension1, $dimension2);
  return empty($diff);
}

/**
 * Sum 2 dimension arrays.
 *
 * @param array $dimension1
 *   Dimension array #1
 * @param $dimension2
 *   Dimension array #2
 *
 * @return array
 *   Dimension array: result of the sum of the 2 provided dimension arrays
 */
function units_dimension_add($dimension1, $dimension2) {
  units_dimension_normalize($dimension1, $dimension2);
  $sum = array();
  foreach ($dimension1 as $k => $v) {
    $sum[$k] = $dimension1[$k] + $dimension2[$k];
  }
  return $sum;
}

/**
 * Subtract 2 dimension arrays.
 *
 * @param array $dimension_minuend
 *   Minuend dimension array
 * @param $dimension_subtrahend
 *   Subtrahend dimension array
 *
 * @return array
 *   Difference dimension array
 */
function units_dimension_subtract($dimension_minuend, $dimension_subtrahend) {
  units_dimension_normalize($dimension_minuend, $dimension_subtrahend);
  $diff = array();
  foreach ($dimension_minuend as $k => $v) {
    $diff[$k] = $dimension_minuend[$k] - $dimension_subtrahend[$k];
  }
  return array_filter($diff);
}

/**
 * Multiply a dimension array by a constant factor.
 *
 * @param array $dimension
 *   Dimension array to be multiplied
 * @param int $multiplier
 *   Constant factor by which to multiply the dimension array
 *
 * @return array
 *   Dimension array: result of the multiplication
 */
function units_dimension_multiply($dimension, $multiplier) {
  foreach ($dimension as $k => $v) {
    $dimension[$k] = $v * $multiplier;
  }
  return $dimension;
}

/**
 * Pre-process 2 dimension arrays.
 *
 * Make sure both have all known dimensions initialized. Sorting them by the
 * dimensions so we can treat them as vectors.
 *
 * @param array $dimension1
 *   Dimension array #1
 * @param $dimension2
 *   Dimension array #2
 */
function units_dimension_normalize(&$dimension1, &$dimension2) {
  $dimension1 += array_fill_keys(array_keys($dimension2), 0);
  $dimension2 += array_fill_keys(array_keys($dimension1), 0);
  ksort($dimension1);
  ksort($dimension2);
}

/**
 * Format one mathematical expression to the format of another.
 *
 * @param \UnitsMathematicalExpression $input
 *   Mathematical expression to be formatted
 * @param \UnitsMathematicalExpression $output
 *   Desired format for $input mathematical expression. Constants of this format
 *   will be filled in based on data stored in $input mathematical expression
 *
 * @throws UnitsMathematicalExpressionDimensionException
 *   Exception is thrown if the input and output mathematical expressions are
 *   of different physical dimension and therefore may not be treated on the
 *   same scale
 */
function units_mathematical_expression_format_as(UnitsMathematicalExpression $input, UnitsMathematicalExpression &$output) {
  if (!units_dimension_equal($input
    ->dimension(), $output
    ->dimension())) {
    throw new UnitsMathematicalExpressionDimensionException();
  }
  $quantity = $input
    ->evaluate();
  if (is_null($quantity)) {

    // Quantity would be null only in the case if $input mathematical expression
    // contains no dimensionless members. It could be the case if $input
    // mathematical expression is an atomic unit or such a composed unit that
    // decomposes into atomic ones without factors, i.e. the decomposition
    // happens only on the physical dimension level. For example, it could be
    // square meter (decomposed into "meter * meter") or alike.
    // To keep things simple for end users we assume "unit" to actually be
    // "1 * unit". That's why we plug in the quantity of 1.
    $quantity = 1;
  }
  $output = $output
    ->formatQuantity($quantity);
}

/**
 * Load mathematical expression from database.
 *
 * @param int $mathematical_expression_id
 *   Mathematical expression to load
 *
 * @return \UnitsMathematicalExpressionWrapper
 *   Loaded mathematical expression that corresponds to the provided
 *   $mathematical_expression_id
 *
 * @throws \UnitsMathematicalExpressionMalformedException
 *   Exception is thrown if the mathematical expression appears to be poorly
 *   stored in database (unknown units, operators or anything alike)
 */
function units_mathematical_expression_load($mathematical_expression_id) {
  if (!$mathematical_expression_id) {
    return NULL;
  }
  $select = db_select('units_mathematical_expression_postfix', 'e');
  $select
    ->fields('e', array(
    'type',
    'value_numeric',
    'value_string',
    'postfix_order',
  ));
  $select
    ->condition('mathematical_expression_id', $mathematical_expression_id);
  $select
    ->orderBy('postfix_order');
  $operators = units_get_operator();
  $stack = array();
  foreach ($select
    ->execute() as $token) {
    switch ($token->type) {
      case UNITS_TOKEN_TYPE_CONSTANT:
        $object = new UnitsConstantMathematicalExpression($token->value_string == UNITS_QUANTITY ? $token->value_string : $token->value_numeric);
        break;
      case UNITS_TOKEN_TYPE_UNIT:
        $object = units_unit_machine_name_load($token->value_string);
        if (!$object) {
          throw new UnitsMathematicalExpressionMalformedException(t('Encountered unknown unit when loading a mathematical expression from database: %unit', array(
            '%unit' => $token->value_string,
          )));
        }
        break;
      case UNITS_TOKEN_TYPE_OPERATOR:
        if (!isset($operators[$token->value_string])) {
          throw new UnitsMathematicalExpressionMalformedException(t('Encountered unknown operator when loading a mathematical expression from database: %operator', array(
            '%operator' => $token->value_string,
          )));
        }
        $operator2 = units_mathematical_expression_postfix_stack_pop($stack);
        $operator1 = units_mathematical_expression_postfix_stack_pop($stack);
        $object = units_mathematical_expression_operator_construct($operators[$token->value_string], $operator1, $operator2);
        break;
      default:
        throw new UnitsMathematicalExpressionMalformedException(t('Encountered unknown token type when loading a mathematical expression from database: %token_type', array(
          '%token_type' => $token->type,
        )));
        break;
    }
    array_push($stack, $object);
  }
  $object = units_mathematical_expression_postfix_stack_pop($stack);
  $wrapper = new UnitsMathematicalExpressionWrapper();
  $wrapper
    ->setExpression($object);
  $wrapper
    ->setMathematicalExpressionId($mathematical_expression_id);
  return $wrapper;
}

/**
 * Delete a mathematical expression from data.
 *
 * @param int $mathematical_expression_id
 *   Mathematical expression ID to be deleted
 */
function units_mathematical_expression_delete($mathematical_expression_id) {
  if (intval($mathematical_expression_id) > 0) {
    db_delete('units_mathematical_expression_postfix')
      ->condition('mathematical_expression_id', $mathematical_expression_id)
      ->execute();
  }
}

/**
 * Create mathematical expression object from infix notation.
 *
 * @param string $infix_mathematical_expression
 *   Mathematical expression written down in infix notation
 *
 * @return \UnitsMathematicalExpressionWrapper
 *   In-memory representation of the provided mathematical expression
 *
 * @throws \UnitsMathematicalExpressionMalformedException
 */
function units_mathematical_expression_create_from_infix($infix_mathematical_expression) {
  $infix_mathematical_expression = array_filter(explode(' ', $infix_mathematical_expression), 'units_mathematical_expression_array_filter');
  if (empty($infix_mathematical_expression)) {
    return NULL;
  }
  $operators = units_get_operator_by_sign();

  // This is an implementation of shunting-yard algorithm.
  $stack = array();
  $output = array();
  foreach ($infix_mathematical_expression as $token) {
    if (is_numeric($token) || $token == UNITS_QUANTITY) {
      $output[] = $token;
    }
    elseif ($unit = units_unit_machine_name_load($token)) {
      $output[] = $unit
        ->toPostfix();
    }
    elseif (isset($operators[$token])) {
      while (!empty($stack) && isset($operators[$stack[count($stack) - 1]]) && $operators[$token]['precedence'] <= $operators[$stack[count($stack) - 1]]['precedence']) {
        $output[] = array_pop($stack);
      }
      $stack[] = $token;
    }
    elseif ($token == '(') {
      $stack[] = $token;
    }
    elseif ($token == ')') {
      while (!empty($stack) && end($stack) != '(') {
        $output[] = array_pop($stack);
      }

      // Popping out the opening parenthesis "(" symbol.
      array_pop($stack);
    }
    else {
      throw new UnitsMathematicalExpressionMalformedException(t('Unknown token: @token', array(
        '@token' => $token,
      )));
    }
  }
  while (!empty($stack)) {
    $output[] = array_pop($stack);
  }
  return units_mathematical_expression_create_from_postfix(implode(' ', $output));
}

/**
 * Create mathematical expression object from postfix (reverse polish) notation.
 *
 * @param string $postfix_mathematical_expression
 *   Mathematical expression written down in postfix (reverse polish) notation
 *
 * @return \UnitsMathematicalExpressionWrapper
 *   In-memory representation of the provided mathematical expression
 *
 * @throws \UnitsMathematicalExpressionMalformedException
 */
function units_mathematical_expression_create_from_postfix($postfix_mathematical_expression) {
  $postfix_mathematical_expression_string = $postfix_mathematical_expression;
  $postfix_mathematical_expression = array_filter(explode(' ', $postfix_mathematical_expression), 'units_mathematical_expression_array_filter');
  if (empty($postfix_mathematical_expression)) {
    return NULL;
  }
  $operators = units_get_operator_by_sign();
  $stack = array();
  foreach ($postfix_mathematical_expression as $token) {
    if (!isset($operators[$token])) {

      // This is either a unit or a constant.
      if (is_numeric($token) || $token == UNITS_QUANTITY) {
        $token = new UnitsConstantMathematicalExpression($token);
      }
      else {
        $unit = units_unit_machine_name_load($token);
        if (!$unit) {
          throw new UnitsMathematicalExpressionMalformedException(t('Unknown token: @token in expression: @expression', array(
            '@token' => $token,
            '@expression' => $postfix_mathematical_expression_string,
          )));
        }
        $token = $unit;
      }
    }
    else {

      // TODO: shouldn't these stack pops be executed through units_mathematical_expression_postfix_stack_pop()?
      $operand2 = array_pop($stack);
      $operand1 = array_pop($stack);
      $token = units_mathematical_expression_operator_construct($operators[$token], $operand1, $operand2);
    }
    array_push($stack, $token);
  }

  // TODO: shouldn't these stack pops be executed through units_mathematical_expression_postfix_stack_pop()?
  $result = array_pop($stack);
  if (!empty($stack)) {

    // The stack must be empty now since we've supposedly processed the whole
    // expression. Be it not the case, we may not trust the $result and better
    // thrown an exception.
    throw new UnitsMathematicalExpressionMalformedException();
  }
  $wrapper = new UnitsMathematicalExpressionWrapper();
  $wrapper
    ->setExpression($result);
  return $wrapper;
}

/**
 * Pop an element off a postfix parsing stack.
 *
 * @param array $stack
 *   Current stack of postfix parsing
 *
 * @return UnitsMathematicalExpression|string
 *   Popped element off the postfix parsing stack. It may be either an instance
 *   of UnitsMathematicalExpression or a string depending on what stage of
 *   parsing we are
 *
 * @throws UnitsMathematicalExpressionMalformedException
 *   Exception is thrown if the stack is empty
 */
function units_mathematical_expression_postfix_stack_pop(&$stack) {
  if (empty($stack)) {
    throw new UnitsMathematicalExpressionMalformedException();
  }
  return array_pop($stack);
}

/**
 * Process function for 'units_mathematical_expression' form element.
 */
function units_mathematical_expression_element_process($element) {
  $element['#tree'] = TRUE;
  $blacklist = array(
    '#type',
    '#element_validate',
    // Make form_builder() regenerate child properties.
    '#parents',
    '#id',
    '#name',
    // Do not copy this #process function to prevent form_builder() from
    // recursing infinitely.
    '#process',
    // Ensure proper ordering of children.
    '#weight',
    // Properties already processed for the parent element.
    '#prefix',
    '#suffix',
    '#attached',
    '#processed',
    '#theme_wrappers',
    // Force form_builder() to load defaults for all children.
    '#defaults_loaded',
    '#value',
  );

  // Move this element into sub-element 'infix'.
  unset($element['infix']);
  foreach (element_properties($element) as $key) {
    if (!in_array($key, $blacklist)) {
      $element['infix'][$key] = $element[$key];
    }
  }
  $element['infix']['#type'] = 'textfield';
  if (isset($element['infix']['#default_value']) && is_object($element['infix']['#default_value'])) {
    $element['infix']['#default_value'] = $element['infix']['#default_value']
      ->getExpression()
      ->toInfix();
  }
  return $element;
}

/**
 * Validate function for 'units_mathematical_expression' form element.
 */
function units_mathematical_expression_element_validate($element, &$form_state) {
  $mathematical_expression = NULL;
  try {
    $mathematical_expression = units_mathematical_expression_create_from_infix($element['infix']['#value']);

    // We evaluate the mathematical expression in order to make sure it contains
    // no dimensional contradictions.
    if (is_object($mathematical_expression)) {
      $mathematical_expression
        ->getExpression()
        ->evaluate();
    }
  } catch (UnitsMathematicalExpressionDimensionException $e) {
    form_error($element, t('%title mathematical expression contains error in dimensions.', array(
      '%title' => $element['#title'],
    )));
  } catch (UnitsMathematicalExpressionMalformedException $e) {
    form_error($element, t('%title contains malformed mathematical expression. @exception', array(
      '%title' => $element['#title'],
      '@exception' => $e
        ->getMessage(),
    )));
  }
  if (is_object($mathematical_expression) && $element['#allowed_dimension'] && !units_dimension_equal($element['#allowed_dimension'], $mathematical_expression
    ->getExpression()
    ->dimension())) {
    form_error($element, t('%title mismatches allowed dimension.', array(
      '%title' => $element['#title'],
    )));
  }
  if (is_object($mathematical_expression)) {
    switch ($element['#value_format']) {
      case 'object':
        break;
      case 'postfix':
        $mathematical_expression = $mathematical_expression
          ->getExpression()
          ->toPostfix();
        break;
      case 'infix':
        $mathematical_expression = $mathematical_expression
          ->getExpression()
          ->toInfix();
        break;
    }
  }
  form_set_value($element, $mathematical_expression, $form_state);
}

/**
 * Initialize DB stored functions necessary for the module.
 *
 * @param bool $create
 *   Whether to create (TRUE) or delete (FALSE) the stored functions
 */
function units_mathematical_expression_recreate_stored_functions($create = TRUE) {

  // TODO: for now we run on PostgreSQL, so we disable this whole thing.
  return;
  $operators = units_get_operator_by_sign();
  $units_token_type_name = 'units_token_type';
  $units_token_type = <<<EOF
CREATE FUNCTION `{<span class="php-variable">$units_token_type_name</span>}` ( token VARCHAR(255) ) RETURNS varchar(255)
BEGIN

IF (token IN (:operator_signs)) THEN
  RETURN ('operator');
ELSEIF EXISTS (SELECT 1 FROM {units_unit} WHERE machine_name = token) THEN
  RETURN ('unit');
ELSE
  RETURN ('constant');
END if;

END
EOF;
  units_mathematical_expression_recreate_stored_function($create, $units_token_type_name, $units_token_type, array(
    ':operator_signs' => array_keys($operators),
  ));
  $units_token_name = 'units_token';
  $units_token = <<<EOF
CREATE FUNCTION `{<span class="php-variable">$units_token_name</span>}` ( expression VARCHAR(255), pos INT(10) ) RETURNS VARCHAR(32)
  DETERMINISTIC
BEGIN

DECLARE length INT;
DECLARE token_length INT;
DECLARE token VARCHAR(32);
DECLARE delimiter VARCHAR(1);

SET delimiter = ' ';

SET expression = CONCAT(expression, delimiter);
SET length = CHAR_LENGTH(expression) + 1;

SET token_length = LOCATE(delimiter, expression, pos) - pos;
SET token = SUBSTRING(expression, pos, token_length);

RETURN (token);

END
EOF;
  units_mathematical_expression_recreate_stored_function($create, $units_token_name, $units_token);
  $units_decompose_postfix_name = 'units_decompose_postfix';
  $units_decompose_postfix = <<<EOF
CREATE FUNCTION `{<span class="php-variable">$units_decompose_postfix_name</span>}` ( expression VARCHAR(255) ) RETURNS VARCHAR(255)
BEGIN

DECLARE i INT;

DECLARE token VARCHAR(32);

SET i = 1;

WHILE i < CHAR_LENGTH(expression) + 1 DO
  SET token = units_token(expression, i);
  SET i = i + CHAR_LENGTH(token) + 1;

  IF (units_token_type(token) = 'unit') THEN
    SELECT REPLACE(expression, machine_name, decomposition_postfix) INTO expression FROM {units_unit} WHERE machine_name = token AND decomposition_postfix <> '' AND decomposition_postfix NOT LIKE :units_quantity;
  END IF;
END WHILE;

RETURN (expression);

END
EOF;
  $units_decompose_postfix_args = array(
    ':units_quantity' => '%' . db_like(UNITS_QUANTITY) . '%',
  );
  units_mathematical_expression_recreate_stored_function($create, $units_decompose_postfix_name, $units_decompose_postfix, $units_decompose_postfix_args);
  $units_evaluate_postfix_iteration_name = 'units_evaluate_postfix_iteration';
  $units_evaluate_postfix_iteration_args = array();
  $units_evaluate_postfix_iteration = array();
  foreach ($operators as $operator) {
    $sign = $operator['sign'];
    $name = $operator['name'];
    $sql_evaluate_fragment = $operator['sql evaluate fragment'];
    $operand_normalization = array();
    for ($i = 1; $i < 3; $i++) {
      if ($operator['transparent operand' . $i]) {
        $operand_normalization[] = "\n        IF (units_token_type(token{$i}) = 'unit') THEN\n          SET token{$i} = :{$name}_token{$i};\n        END IF;\n        ";
        $units_evaluate_postfix_iteration_args[$name . '_token' . $i] = $operator['transparent operand' . $i];
      }
    }
    $operand_normalization = implode("\n", $operand_normalization);
    $units_evaluate_postfix_iteration[] = <<<EOF
    WHEN '{<span class="php-variable">$sign</span>}' THEN
      {<span class="php-variable">$operand_normalization</span>}
      {<span class="php-variable">$sql_evaluate_fragment</span>}
EOF;
  }
  $units_evaluate_postfix_iteration = implode("\n", $units_evaluate_postfix_iteration);
  $units_evaluate_postfix_iteration = <<<EOF
CREATE FUNCTION `{<span class="php-variable">$units_evaluate_postfix_iteration_name</span>}` ( expression VARCHAR(255) ) RETURNS VARCHAR(255)
BEGIN

DECLARE i INT;

DECLARE token1 VARCHAR(32);
DECLARE token2 VARCHAR(32);
DECLARE token3 VARCHAR(32);

DECLARE token1_length INT;
DECLARE token2_length INT;
DECLARE token3_length INT;

DECLARE result FLOAT;

SET i = 1;

WHILE i < CHAR_LENGTH(expression) + 1 DO
  SET token1 = units_token(expression, i);
  SET token1_length = CHAR_LENGTH(token1);

  SET token2 = units_token(expression, i + token1_length + 1);
  SET token2_length = CHAR_LENGTH(token2);

  SET token3 = units_token(expression, i + token1_length + 1 + token2_length + 1);
  SET token3_length = CHAR_LENGTH(token3);

  IF (units_token_type(token1) IN ('constant', 'unit')) AND (units_token_type(token2) IN ('constant', 'unit')) AND (units_token_type(token3) = 'operator') THEN
    CASE token3
{<span class="php-variable">$units_evaluate_postfix_iteration</span>}
    END CASE;
    SET expression = CONCAT(SUBSTRING(expression, 1, i - 1), result, SUBSTRING(expression, i + token1_length + 1 + token2_length + 1 + token3_length + 1 - 1));
  ELSE
    SET i = i + token1_length + 1;
  END IF;

END WHILE;

RETURN (expression);

END
EOF;
  units_mathematical_expression_recreate_stored_function($create, $units_evaluate_postfix_iteration_name, $units_evaluate_postfix_iteration, $units_evaluate_postfix_iteration_args);
  $units_evaluate_postfix_name = 'units_evaluate_postfix';
  $units_evaluate_postfix = <<<EOF
CREATE FUNCTION `{<span class="php-variable">$units_evaluate_postfix_name</span>}` ( expression VARCHAR(255) ) RETURNS FLOAT
BEGIN

DECLARE old_expression VARCHAR(255);

SET expression = units_decompose_postfix(expression);

REPEAT
  SET old_expression = expression;
  SET expression = units_evaluate_postfix_iteration(expression);
UNTIL old_expression = expression END REPEAT;

RETURN (expression);

END
EOF;
  units_mathematical_expression_recreate_stored_function($create, $units_evaluate_postfix_name, $units_evaluate_postfix);
}

/**
 * Initialize a particular stored function.
 *
 * @param bool $create
 *   Whether to create (TRUE) or to delete (FALSE) the stored function
 * @param string $name
 *   Name of the stored function
 * @param string $definition
 *   SQL definition of the stored function
 * @param array $definition_args
 *   Any parameters that accompany $definition SQL query
 */
function units_mathematical_expression_recreate_stored_function($create, $name, $definition, $definition_args = array()) {
  db_query("DROP FUNCTION IF EXISTS `{$name}`");
  if ($create) {
    db_query($definition, $definition_args);
  }
}

/**
 * Construct operator object.
 *
 * @param array $operator
 *   Definition of cTools "operator" plugin whose object should be constructed
 * @param \UnitsMathematicalExpression $operand1
 *   Operand #1
 * @param \UnitsMathematicalExpression $operand2
 *   Operand #2
 *
 * @return \UnitsMathematicalOperatorInterface
 *   Object that represents provided input arguments
 */
function units_mathematical_expression_operator_construct(array $operator, UnitsMathematicalExpression $operand1, UnitsMathematicalExpression $operand2) {
  $class = $operator['operator class'];
  return new $class($operator, $operand1, $operand2);
}

/**
 * Filter infix/postfix annotated mathematical expressions.
 *
 * @param string $token
 *   Token to filter
 *
 * @return bool
 *   Whether the token is accepted or should be disregarded
 */
function units_mathematical_expression_array_filter($token) {
  return $token !== '';
}

/**
 * Interface of "mathematical expression".
 */
interface UnitsMathematicalExpression {

  /**
   * Determine physical dimension of this mathematical expression.
   *
   * @return array
   *   Dimension array of this mathematical expression
   */
  public function dimension();

  /**
   * Test whether this mathematical expression includes a dimensionless member.
   *
   * Whether this mathematical expression contains at least 1 dimensionless
   * member.
   *
   * @return bool
   *   Whether this mathematical expression contains at least 1 dimensionless
   *   member
   */
  public function containsDimensionlessMember();

  /**
   * Format a certain amount of quantity within this mathematical expression.
   *
   * @param float $quantity
   *   Quantity to be formatted
   *
   * @return UnitsMathematicalExpression
   *   Formatted quantity into this mathematical expression. Sometimes the
   *   mathematical expression itself must mutate in order to format the
   *   quantity. So the returned mathematical expression may not necessarily be
   *   the mathematical expression on which this method was invoked. For
   *   example, the expression "unit" would mutate into "1 * unit" in order to
   *   have a dimensionless member and therefore be able to format the $quantity
   */
  public function formatQuantity($quantity);

  /**
   * Numerically evaluate this mathematical expression.
   *
   * @return float
   *   Numerical value of this mathematical expression
   */
  public function evaluate();

  /**
   * Decompose (simplify) this mathematical expression.
   *
   * @return UnitsMathematicalExpression
   *   Decomposed (simplified) version of this mathematical expression
   */
  public function decompose();

  /**
   * Represent this mathematical expression in postfix notation.
   *
   * Represent this mathematical expression in postfix (reverse polish)
   * notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *   TODO: document possible options
   *
   * @return string
   *   Postfix (reverse polish) notation representation of this mathematical
   *   expression
   */
  public function toPostfix($options = array());

  /**
   * Represent this mathematical expression in human-friendly infix notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *   TODO: document possible options
   *
   * @return string
   *   Human-friendly formatted version of this mathematical expression taking
   *   into account provided $options
   */
  public function toInfix($options = array());

  /**
   * Whether this expression is linearly decomposable.
   *
   * @return bool
   *   Whether this expression is linearly decomposable, i.e. its decomposition
   *   can be plugged in into another mathematical expression via multiplication
   *   without losing mathematical sense
   */
  public function isLinear();

  /**
   * Save the mathematical expression into database.
   *
   * @param int $mathematical_expression_id
   *   If the ID of the mathematical expression is known, under which it should
   *   be saved, provide it here. Otherwise it will be generated automatically
   * @param int $order
   *   Order of this member when the mathematical expression is written down in
   *   postfix notation. Primarily this argument is used for internal purposes
   *
   * @return int
   *   Mathematical expression ID under which this expression has been saved in
   *   the database
   */
  public function unitsMathematicalExpressionSave($mathematical_expression_id, &$order);

}

/**
 * Interface of mathematical operator.
 */
interface UnitsMathematicalOperatorInterface extends UnitsMathematicalExpression {

  /**
   * Retrieve operand #1 from this mathematical operator.
   *
   * @return UnitsMathematicalExpression
   *   Operand #1 from this mathematical expression
   */
  public function operand1();

  /**
   * Retrieve operand #2 from this mathematical operator.
   *
   * @return UnitsConstantMathematicalExpression
   *   Operand #2 from this mathematical expression
   */
  public function operand2();

  /**
   * Numerically isolate a certain operand of this mathematical expression.
   *
   * Given result of this mathematical expression, numerically, i.e.
   * dimensionlessly isolate a certain operand of this mathematical expression.
   *
   * @param \UnitsMathematicalExpression $equal_expression
   *   Result of this mathematical expression
   * @param int $operand_to_isolate
   *   What operand of this mathematical expression to isolate. Allowed values
   *   are:
   *   - 1: to isolate operand #1
   *   - 2: to isolate operand #2
   *
   * @return UnitsMathematicalExpression
   *   Mathematical expression to which the isolated member numerically equals
   */
  public function isolateOperand(UnitsMathematicalExpression $equal_expression, $operand_to_isolate);

}

/**
 * Abstract implementation of "mathematical operator" interface.
 */
abstract class AbstractUnitsMathematicalOperator implements UnitsMathematicalOperatorInterface {

  /**
   * Definition of the underlying cTools 'operator' plugin.
   *
   * @var array
   */
  protected $operator;

  /**
   * Operand #1 of this mathematical expression.
   *
   * @var UnitsMathematicalExpression
   */
  protected $operand1;

  /**
   * Operand #2 of this mathematical expression.
   *
   * @var UnitsMathematicalExpression
   */
  protected $operand2;

  /**
   * UnitsOperatorMathematicalExpression constructor.
   *
   * @param array $operator
   *   Definition of a cTools 'operator' plugin that represents the specific
   *   operator
   * @param \UnitsMathematicalExpression $operand1
   *   Operand #1 for this mathematical expression
   * @param \UnitsMathematicalExpression $operand2
   *   Operand #2 for this mathematical expression
   */
  public function __construct($operator, UnitsMathematicalExpression $operand1, UnitsMathematicalExpression $operand2) {
    $this->operator = $operator;
    $this->operand1 = $operand1;
    $this->operand2 = $operand2;
  }

  /**
   * Determine physical dimension of this mathematical expression.
   *
   * @return array
   *   Dimension array of this mathematical expression
   */
  public function dimension() {
    $dimension_callback = ctools_plugin_get_function($this->operator, 'dimension callback');
    list($evaluate1, $evaluate2) = $this
      ->evaluateOperands();
    return $dimension_callback($this->operand1
      ->dimension(), $this->operand2
      ->dimension(), $evaluate1, $evaluate2);
  }

  /**
   * Test whether this mathematical expression includes a dimensionless member.
   *
   * Whether this mathematical expression contains at least 1 dimensionless
   * member.
   *
   * @return bool
   *   Whether this mathematical expression contains at least 1 dimensionless
   *   member
   */
  public function containsDimensionlessMember() {
    return $this->operand1
      ->containsDimensionlessMember() || $this->operand2
      ->containsDimensionlessMember();
  }

  /**
   * Format a certain amount of quantity within this mathematical expression.
   *
   * @param float $quantity
   *   Quantity to be formatted
   *
   * @return UnitsMathematicalExpression
   *   Formatted quantity into this mathematical expression. Sometimes the
   *   mathematical expression itself must mutate in order to format the
   *   quantity. So the returned mathematical expression may not necessarily be
   *   the mathematical expression on which this method was invoked. For
   *   example, the expression "unit" would mutate into "1 * unit" in order to
   *   have a dimensionless member and therefore be able to format the $quantity
   */
  public function formatQuantity($quantity) {
    $contains_dimensionless1 = $this->operand1
      ->containsDimensionlessMember();
    $contains_dimensionless2 = $this->operand2
      ->containsDimensionlessMember();
    list($quantity1, $quantity2) = $this
      ->evaluateOperands();
    if ($contains_dimensionless1 xor $contains_dimensionless2) {
      if ($contains_dimensionless1) {
        $this->operand1
          ->formatQuantity($quantity / $quantity2);
      }
      else {
        $this->operand2
          ->formatQuantity($quantity / $quantity1);
      }
    }
    else {
      $split_quantity = ctools_plugin_get_function($this->operator, 'split quantity callback');
      list($quantity1, $quantity2) = $split_quantity($quantity, $quantity1, $quantity2, $this->operator);
      $this->operand1
        ->formatQuantity($quantity1);
      $this->operand2
        ->formatQuantity($quantity2);
    }
    return $this;
  }

  /**
   * Numerically evaluate this mathematical expression.
   *
   * @return float
   *   Numerical value of this mathematical expression
   *
   * @throws UnitsMathematicalExpressionDimensionException
   *   Exception is thrown if this mathematical expression has inconsistency in
   *   physical dimensions
   */
  public function evaluate() {
    if ($this->operator['dimension check'] && !units_dimension_equal($this->operand1
      ->dimension(), $this->operand2
      ->dimension())) {
      throw new UnitsMathematicalExpressionDimensionException();
    }
    list($evaluate1, $evaluate2) = $this
      ->evaluateOperands();
    $evaluate_callback = ctools_plugin_get_function($this->operator, 'evaluate callback');
    return $evaluate_callback($evaluate1, $evaluate2);
  }

  /**
   * Decompose (simplify) this mathematical expression.
   *
   * @return UnitsMathematicalExpression
   *   Decomposed (simplified) version of this mathematical expression
   */
  public function decompose() {
    return units_mathematical_expression_operator_construct($this->operator, $this
      ->operand1()
      ->decompose(), $this
      ->operand2()
      ->decompose());
  }

  /**
   * Represent this mathematical expression in postfix notation.
   *
   * Represent this mathematical expression in postfix (reverse polish)
   * notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *
   * @return string
   *   Postfix (reverse polish) notation representation of this mathematical
   *   expression
   */
  public function toPostfix($options = array()) {
    return implode(' ', array(
      $this->operand1
        ->toPostfix(),
      $this->operand2
        ->toPostfix(),
      $this->operator['sign'],
    ));
  }

  /**
   * Represent this mathematical expression in human-friendly infix notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *
   * @return string
   *   Human-friendly formatted version of this mathematical expression taking
   *   into account provided $options
   */
  public function toInfix($options = array()) {
    $child_options = array(
      'parent precedence' => $this->operator['precedence'],
    ) + $options;
    $output = implode(' ', array(
      $this->operand1
        ->toInfix(array(
        'operand' => 1,
      ) + $child_options),
      $this->operator['sign'],
      $this->operand2
        ->toInfix(array(
        'operand' => 2,
      ) + $child_options),
    ));
    if (isset($options['parent precedence']) && ($options['parent precedence'] > $this->operator['precedence'] || $options['operand'] == 2 && $options['parent precedence'] == $this->operator['precedence'])) {
      $output = '( ' . $output . ' )';
    }
    return $output;
  }

  /**
   * Whether this expression is linearly decomposable.
   *
   * @return bool
   *   Whether this expression is linearly decomposable, i.e. its decomposition
   *   can be plugged in into another mathematical expression via multiplication
   *   without losing sense
   */
  public function isLinear() {
    return TRUE;
  }

  /**
   * Save the mathematical expression into database.
   *
   * @param int $mathematical_expression_id
   *   If the ID of the mathematical expression is known, under which it should
   *   be saved, provide it here. Otherwise it will be generated automatically
   * @param int $order
   *   Order of this member when the mathematical expression is written down in
   *   postfix notation. Primarily this argument is used for internal purposes
   *
   * @return int
   *   Mathematical expression ID under which this expression has been saved in
   *   the database
   */
  public function unitsMathematicalExpressionSave($mathematical_expression_id, &$order) {
    $mathematical_expression_id = $this
      ->operand1()
      ->unitsMathematicalExpressionSave($mathematical_expression_id, $order);
    $this
      ->operand2()
      ->unitsMathematicalExpressionSave($mathematical_expression_id, $order);
    db_insert('units_mathematical_expression_postfix')
      ->fields(array(
      'mathematical_expression_id' => $mathematical_expression_id,
      'type' => UNITS_TOKEN_TYPE_OPERATOR,
      'value_string' => $this->operator['name'],
      'postfix_order' => ++$order,
    ))
      ->execute();
    return $mathematical_expression_id;
  }

  /**
   * Retrieve operand #1 from this mathematical operator.
   *
   * @return UnitsMathematicalExpression
   *   Operand #1 from this mathematical expression
   */
  public function operand1() {
    return $this->operand1;
  }

  /**
   * Retrieve operand #2 from this mathematical operator.
   *
   * @return UnitsConstantMathematicalExpression
   *   Operand #2 from this mathematical expression
   */
  public function operand2() {
    return $this->operand2;
  }

  /**
   * Numerically isolate a certain operand of this mathematical expression.
   *
   * Given result of this mathematical expression, numerically, i.e.
   * dimensionlessly isolate a certain operand of this mathematical expression.
   *
   * @param \UnitsMathematicalExpression $equal_expression
   *   Result of this mathematical expression
   * @param int $operand_to_isolate
   *   What operand of this mathematical expression to isolate. Allowed values
   *   are:
   *   - 1: to isolate operand #1
   *   - 2: to isolate operand #2
   *
   * @return UnitsConstantMathematicalExpression
   *   Mathematical expression to which the isolated member numerically equals
   */
  public function isolateOperand(UnitsMathematicalExpression $equal_expression, $operand_to_isolate) {
    $isolate_callback = ctools_plugin_get_function($this->operator, 'isolate operand callback');
    if ($isolate_callback) {
      list($operand1, $operand2) = $this
        ->evaluateOperands();
      if ($operand_to_isolate == 1) {
        $operand1 = $this
          ->operand1();
        $operand2 = new UnitsConstantMathematicalExpression($operand2);
      }
      else {
        $operand1 = new UnitsConstantMathematicalExpression($operand1);
        $operand2 = $this
          ->operand2();
      }
      return $isolate_callback($operand1, $operand2, $equal_expression, $operand_to_isolate);
    }
  }

  /**
   * Numerically evaluate both operands and return them as an array.
   *
   * @return array
   *   Array of length 2: the 2 operands numerically evaluated
   */
  protected function evaluateOperands() {
    $evaluate1 = $this->operand1
      ->evaluate();
    $evaluate2 = $this->operand2
      ->evaluate();
    if (is_null($evaluate1)) {
      $evaluate1 = $this->operator['transparent operand1'];
    }
    if (is_null($evaluate2)) {
      $evaluate2 = $this->operator['transparent operand2'];
    }
    return array(
      $evaluate1,
      $evaluate2,
    );
  }

}

/**
 * Implementation of "mathematical operator" interface for a linear operation.
 *
 * Implementation of "mathematical operator" interface for a linear mathematical
 * operation.
 */
class UnitsMathematicalOperatorLinear extends AbstractUnitsMathematicalOperator {

}

/**
 * Implementation of "mathematical operator" interface for non-linear operation.
 *
 * Implementation of "mathematical operator" interface for a non-linear
 * mathematical operation.
 */
class UnitsMathematicalOperatorNonLinear extends AbstractUnitsMathematicalOperator {

  /**
   * Constructor.
   *
   * @param array $operator
   *   Definition of a cTools 'operator' plugin that represents the specific
   *   operator
   * @param \UnitsConstantMathematicalExpression $operand1
   *   Operand #1 for this mathematical expression
   * @param \UnitsEntity $operand2
   *   Operand #2 for this mathematical expression
   *
   * @throws UnitsMathematicalExpressionMalformedException
   *   Exception is thrown if the linearity is not consistent (none or both
   *   non-linear operands)
   */
  public function __construct(array $operator, \UnitsConstantMathematicalExpression $operand1, \UnitsEntity $operand2) {
    parent::__construct($operator, $operand1, $operand2);
    if ($this
      ->nonLinearMember()
      ->isLinear()) {
      throw new UnitsMathematicalExpressionMalformedException(t('Expression %expression operates on 2 linear members while the operator is non-linear.', array(
        '%expression' => $this
          ->toInfix(),
      )));
    }
  }

  /**
   * Determine physical dimension of this mathematical expression.
   *
   * @return array
   *   Dimension array of this mathematical expression
   */
  public function dimension() {
    return $this
      ->nonLinearMember()
      ->dimension();
  }

  /**
   * Format a certain amount of quantity within this mathematical expression.
   *
   * @param float $quantity
   *   Quantity to be formatted
   *
   * @return UnitsMathematicalExpression
   *   Formatted quantity into this mathematical expression. Sometimes the
   *   mathematical expression itself must mutate in order to format the
   *   quantity. So the returned mathematical expression may not necessarily be
   *   the mathematical expression on which this method was invoked. For
   *   example, the expression "unit" would mutate into "1 * unit" in order to
   *   have a dimensionless member and therefore be able to format the $quantity
   */
  public function formatQuantity($quantity) {
    $unit = $this
      ->nonLinearMember();
    $decomposition = $unit
      ->inverseDecompose();

    // Swap the quantity placeholder with the real value.
    $decomposition = units_mathematical_expression_create_from_postfix(str_replace(UNITS_QUANTITY, $quantity, $decomposition
      ->toPostfix()))
      ->getExpression();
    $this
      ->linearMember()
      ->formatQuantity($decomposition
      ->evaluate());
    return $this;
  }

  /**
   * Numerically evaluate this mathematical expression.
   *
   * @return float
   *   Numerical value of this mathematical expression
   */
  public function evaluate() {
    return $this
      ->decompose()
      ->evaluate();
  }

  /**
   * Decompose (simplify) this mathematical expression.
   *
   * @return UnitsMathematicalExpression
   *   Decomposed (simplified) version of this mathematical expression
   */
  public function decompose() {
    $unit = $this
      ->nonLinearMember();

    // This is somewhat hacky, but it's the best I could come up with. We will
    // temporarily swap decomposition expression in the unit with the specific
    // quantity and then will invoke the decomposition on the unit.
    $temporary_decomposition = $unit->decomposition;
    $unit->decomposition = units_mathematical_expression_create_from_postfix(str_replace(UNITS_QUANTITY, $this
      ->linearMember()
      ->toPostfix(), $unit->decomposition
      ->getExpression()
      ->toPostfix()));
    $decomposition = $unit
      ->decompose();
    $unit->decomposition = $temporary_decomposition;
    return $decomposition;
  }

  /**
   * Whether this expression is linearly decomposable.
   *
   * @return bool
   *   Whether this expression is linearly decomposable, i.e. its decomposition
   *   can be plugged in into another mathematical expression via multiplication
   *   without losing sense
   */
  public function isLinear() {
    return FALSE;
  }

  /**
   * Fetch the linear member of the non-linear operator.
   *
   * @return \UnitsConstantMathematicalExpression
   *   The linear member of the non-linear operator, which is always a constant
   */
  public function linearMember() {
    return $this->operand1;
  }

  /**
   * Fetch the non-linear member of the non-linear operator.
   *
   * @return \UnitsEntity
   *   The non-linear member of the non-linear operator, which is always a
   *   non-linear unit entity
   */
  public function nonLinearMember() {
    return $this->operand2;
  }

}

/**
 * Implementation of "mathematical expression" interface for a constant.
 *
 * Implementation of "mathematical expression" interface for a dimensionless
 * constant.
 */
class UnitsConstantMathematicalExpression implements UnitsMathematicalExpression {

  /**
   * Constant that is held in this mathematical expression.
   *
   * @var float
   */
  protected $constant;

  /**
   * UnitsConstantMathematicalExpression constructor.
   *
   * @param int|float $constant
   *   Constant that should be held in this mathematical expression
   */
  public function __construct($constant) {
    $this->constant = $constant;
  }

  /**
   * Determine physical dimension of this mathematical expression.
   *
   * @return array
   *   Dimension array of this mathematical expression
   */
  public function dimension() {
    return array();
  }

  /**
   * Test whether this mathematical expression includes a dimensionless member.
   *
   * Whether this mathematical expression contains at least 1 dimensionless
   * member.
   *
   * @return bool
   *   Whether this mathematical expression contains at least 1 dimensionless
   *   member
   */
  public function containsDimensionlessMember() {
    return TRUE;
  }

  /**
   * Format a certain amount of quantity within this mathematical expression.
   *
   * @param float $quantity
   *   Quantity to be formatted
   *
   * @return UnitsMathematicalExpression
   *   Formatted quantity into this mathematical expression. Sometimes the
   *   mathematical expression itself must mutate in order to format the
   *   quantity. So the returned mathematical expression may not necessarily be
   *   the mathematical expression on which this method was invoked. For
   *   example, the expression "unit" would mutate into "1 * unit" in order to
   *   have a dimensionless member and therefore be able to format the $quantity
   */
  public function formatQuantity($quantity) {
    $this->constant = $quantity;
    return $this;
  }

  /**
   * Numerically evaluate this mathematical expression.
   *
   * @return float
   *   Numerical value of this mathematical expression
   */
  public function evaluate() {
    return $this->constant;
  }

  /**
   * Decompose (simplify) this mathematical expression.
   *
   * @return UnitsMathematicalExpression
   *   Decomposed (simplified) version of this mathematical expression
   */
  public function decompose() {
    return $this;
  }

  /**
   * Represent this mathematical expression in postfix notation.
   *
   * Represent this mathematical expression in postfix (reverse polish)
   * notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *
   * @return string
   *   Postfix (reverse polish) notation representation of this mathematical
   *   expression
   */
  public function toPostfix($options = array()) {
    return $this->constant;
  }

  /**
   * Represent this mathematical expression in human-friendly infix notation.
   *
   * @param array $options
   *   Options regarding how to format this mathematical expression
   *
   * @return string
   *   Human-friendly formatted version of this mathematical expression taking
   *   into account provided $options
   */
  public function toInfix($options = array()) {
    return $this->constant;
  }

  /**
   * Whether this expression is linearly decomposable.
   *
   * @return bool
   *   Whether this expression is linearly decomposable, i.e. its decomposition
   *   can be plugged in into another mathematical expression via multiplication
   *   without losing sense
   */
  public function isLinear() {
    return TRUE;
  }

  /**
   * Save the mathematical expression into database.
   *
   * @param int $mathematical_expression_id
   *   If the ID of the mathematical expression is known, under which it should
   *   be saved, provide it here. Otherwise it will be generated automatically
   * @param int $order
   *   Order of this member when the mathematical expression is written down in
   *   postfix notation. Primarily this argument is used for internal purposes
   *
   * @return int
   *   Mathematical expression ID under which this expression has been save in
   *   the database
   */
  public function unitsMathematicalExpressionSave($mathematical_expression_id, &$order) {
    if (!$mathematical_expression_id) {

      // TODO: this should be possible to do as: INSERT INTO ... FROM SELECT ... thereby making unnecessary the transaction.
      // See https://www.drupal.org/node/310079 for more details.
      $transaction = db_transaction();
      $select = db_select('units_mathematical_expression_postfix', 'e');
      $select
        ->addExpression('MAX(e.mathematical_expression_id) + 1', 'mathematical_expression_id');
      $mathematical_expression_id = $select
        ->execute()
        ->fetchField();
      if (!$mathematical_expression_id) {
        $mathematical_expression_id = 1;
      }
    }
    db_insert('units_mathematical_expression_postfix')
      ->fields(array(
      'type' => UNITS_TOKEN_TYPE_CONSTANT,
      'mathematical_expression_id' => $mathematical_expression_id,
      $this->constant == UNITS_QUANTITY ? 'value_string' : 'value_numeric' => $this->constant,
      'postfix_order' => ++$order,
    ))
      ->execute();
    return $mathematical_expression_id;
  }

}

/**
 * Exception thrown whenever a units mathematical expression is malformed.
 */
class UnitsMathematicalExpressionMalformedException extends Exception {

}

/**
 * Exception thrown whenever there is a physical dimension inconsistency.
 *
 * Exception thrown whenever a units mathematical expression contains physical
 * dimension inconsistency.
 */
class UnitsMathematicalExpressionDimensionException extends UnitsMathematicalExpressionMalformedException {

}

/**
 * Wrapper around "mathematical expression" objects for ease of db storage.
 */
class UnitsMathematicalExpressionWrapper {

  /**
   * Mathematical expression ID of this mathematical expression, if known.
   *
   * @var int
   */
  protected $mathematical_expression_id;

  /**
   * Pointer to the underlying mathematical expression object.
   *
   * @var UnitsMathematicalExpression
   */
  protected $expression;

  /**
   * Get underlying mathematical expression.
   *
   * @return \UnitsMathematicalExpression
   *   The underlying mathematical expression
   */
  public function getExpression() {
    return $this->expression;
  }

  /**
   * Set underlying mathematical expression.
   *
   * @param \UnitsMathematicalExpression $expression
   *   Mathematical expression to set as underlying one
   */
  public function setExpression(UnitsMathematicalExpression $expression) {
    $this->expression = $expression;
  }

  /**
   * Save the underlying mathematical expression into database.
   */
  public function save() {
    if ($this
      ->getMathematicalExpressionId()) {
      units_mathematical_expression_delete($this
        ->getMathematicalExpressionId());
    }
    $order = 0;
    $this
      ->setMathematicalExpressionId($this
      ->getExpression()
      ->unitsMathematicalExpressionSave($this
      ->getMathematicalExpressionId(), $order));
  }

  /**
   * Get mathematical expression ID of this mathematical expression.
   *
   * @return int
   *   Mathematical expression ID of this mathematical expression
   */
  public function getMathematicalExpressionId() {
    return $this->mathematical_expression_id;
  }

  /**
   * Set mathematical expression ID of this mathematical expression.
   *
   * When saving this mathematical expression into database the old entries of
   * this mathematical expression ID will be removed prior saving the in-memory
   * representation.
   *
   * @param int $mathematical_expression_id
   *   Mathematical expression ID of this mathematical expression
   */
  public function setMathematicalExpressionId($mathematical_expression_id) {
    $this->mathematical_expression_id = $mathematical_expression_id;
  }

}

Functions

Namesort descending Description
units_ctools_plugin_directory Implements hook_ctools_plugin_directory().
units_ctools_plugin_type Implements hook_ctools_plugin_type().
units_dimension_add Sum 2 dimension arrays.
units_dimension_equal Check whether the 2 dimensions are equal.
units_dimension_multiply Multiply a dimension array by a constant factor.
units_dimension_normalize Pre-process 2 dimension arrays.
units_dimension_subtract Subtract 2 dimension arrays.
units_element_info Implements hook_element_info().
units_entity_access Access callback for entity types 'units_measure' and 'units_unit'.
units_entity_info Implements hook_entity_info().
units_get_operator Retrieve information about a cTools plugin - units operator.
units_get_operator_by_sign Retrieve information about a cTools plugin - units operator.
units_mathematical_expression_array_filter Filter infix/postfix annotated mathematical expressions.
units_mathematical_expression_create_from_infix Create mathematical expression object from infix notation.
units_mathematical_expression_create_from_postfix Create mathematical expression object from postfix (reverse polish) notation.
units_mathematical_expression_delete Delete a mathematical expression from data.
units_mathematical_expression_element_process Process function for 'units_mathematical_expression' form element.
units_mathematical_expression_element_validate Validate function for 'units_mathematical_expression' form element.
units_mathematical_expression_format_as Format one mathematical expression to the format of another.
units_mathematical_expression_load Load mathematical expression from database.
units_mathematical_expression_operator_construct Construct operator object.
units_mathematical_expression_postfix_stack_pop Pop an element off a postfix parsing stack.
units_mathematical_expression_recreate_stored_function Initialize a particular stored function.
units_mathematical_expression_recreate_stored_functions Initialize DB stored functions necessary for the module.
units_measure_delete Delete a single entity of type 'units_measure'.
units_measure_delete_multiple Delete multiple entities of type 'units_measure'.
units_measure_load Load an entity of entity type 'units_measure' by its ID.
units_measure_load_multiple Load multiple entities of entity type 'units_unit'.
units_measure_machine_name_load Load a single entity of type 'units_measure' loading by its machine name.
units_measure_save Save an entity of type 'units_measure'.
units_units_measure_delete Implements hook_ENTITY_TYPE_delete().
units_units_measure_update Implements hook_ENTITY_TYPE_update().
units_units_unit_delete Implements hook_ENTITY_TYPE_delete().
units_units_unit_load Implements hook_ENTITY_TYPE_load().
units_units_unit_presave Implements hook_ENTITY_TYPE_presave().
units_unit_by_measure_load_multiple Load all units of the supplied measure.
units_unit_delete Delete a single entity of type 'units_unit'.
units_unit_delete_multiple Delete multiple entities of type 'units_unit'.
units_unit_load Load an entity of entity type 'units_unit' by its ID.
units_unit_load_multiple Load multiple entities of entity type 'units_unit'.
units_unit_machine_name_load Load a single entity of type 'units_unit' loading by its machine name.
units_unit_save Save an entity of type 'units_unit'.

Constants

Namesort descending Description
UNITS_QUANTITY Denote placeholder for quantity to use in decomposition of non-linear units.
UNITS_TOKEN_TYPE_CONSTANT Denote constant token type.
UNITS_TOKEN_TYPE_OPERATOR Denote operator token type.
UNITS_TOKEN_TYPE_UNIT Denote unit token type.

Classes

Namesort descending Description
AbstractUnitsMathematicalOperator Abstract implementation of "mathematical operator" interface.
UnitsConstantMathematicalExpression Implementation of "mathematical expression" interface for a constant.
UnitsMathematicalExpressionDimensionException Exception thrown whenever there is a physical dimension inconsistency.
UnitsMathematicalExpressionMalformedException Exception thrown whenever a units mathematical expression is malformed.
UnitsMathematicalExpressionWrapper Wrapper around "mathematical expression" objects for ease of db storage.
UnitsMathematicalOperatorLinear Implementation of "mathematical operator" interface for a linear operation.
UnitsMathematicalOperatorNonLinear Implementation of "mathematical operator" interface for non-linear operation.

Interfaces

Namesort descending Description
UnitsMathematicalExpression Interface of "mathematical expression".
UnitsMathematicalOperatorInterface Interface of mathematical operator.