You are here

hms_field.module in HMS Field 7

Same filename and directory in other branches
  1. 8 hms_field.module

Provides an hms_field functionality.

File

hms_field.module
View source
<?php

/**
 * @file
 * Provides an hms_field functionality.
 */

/**
 * Implements hook_element_info().
 */
function hms_field_element_info() {
  return array(
    'hms' => array(
      '#input' => TRUE,
      '#size' => 8,
      '#maxlength' => 16,
      '#default_value' => NULL,
      '#autocomplete_path' => FALSE,
      '#process' => array(
        'ajax_process_form',
      ),
      '#theme' => 'textfield',
      '#theme_wrappers' => array(
        'form_element',
      ),
      '#pre_render' => array(
        '_hms_pre_render_form_element',
      ),
      '#value_callback' => '_hms_value_callback',
      '#format' => 'h:mm:ss',
      '#element_validate' => array(
        '_hms_validate_form_element',
      ),
    ),
  );
}

/**
 * Implements hook_views_api().
 */
function hms_field_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'hms_field') . '/views',
  );
}

/**
 * Implements hook_field_info().
 */
function hms_field_field_info() {
  return array(
    'hms_field' => array(
      'label' => t('Hours Minutes and Seconds'),
      'description' => t('Store Hours minutes or Seconds'),
      'translatable' => 0,
      'settings' => array(),
      'instance_settings' => array(
        'format' => 'h:mm',
        'default_description' => 1,
      ),
      'default_widget' => 'hms_default_widget',
      'default_formatter' => 'hms_default_formatter',
      'property_type' => 'integer',
    ),
  );
}

/**
 * Implements hook_field_instance_settings_form().
 */
function hms_field_field_instance_settings_form($field, $instance) {
  return array(
    'format' => array(
      '#type' => 'select',
      '#title' => t('Input format'),
      '#default_value' => $instance['settings']['format'],
      '#options' => _hms_format_options(),
      '#description' => t('The input format used for this field. Decimal number can be used separated with dot (e.g. 0,25 = 15 minutes)'),
    ),
    'default_description' => array(
      '#type' => 'checkbox',
      '#title' => t('Default help text'),
      '#default_value' => $instance['settings']['default_description'],
      '#description' => t('Provide a default help text about the format. (Only when you leave the help text empty.)'),
    ),
  );
}

/**
 * Implements hook_field_widget_info().
 */
function hms_field_field_widget_info() {
  return array(
    'hms_default_widget' => array(
      'label' => t('Hour Minutes and Seconds'),
      'field types' => array(
        'hms_field',
      ),
    ),
  );
}

/**
 * Implements hook_field_is_empty().
 */
function hms_field_field_is_empty($item, $field) {
  if ($item['value'] === '') {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_field_widget_form().
 */
function hms_field_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $value = isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL;
  $format = $instance['settings']['format'];
  $default_description = $instance['settings']['default_description'];
  $widget = $element;
  $widget['#delta'] = $delta;
  $widget += array(
    '#type' => 'hms',
    '#default_value' => $value,
    '#format' => $format,
  );
  if ($default_description && (!isset($widget['#description']) || !strlen($widget['#description']))) {
    $widget['#description'] = t('Input format: @format. Decimal number can be used separated with dot (e.g. 0,25 = 15 minutes)', array(
      '@format' => $format,
    ));
  }
  $element['value'] = $widget;
  return $element;
}

/**
 * Implements hook_field_formatter_info().
 */
function hms_field_field_formatter_info() {
  return array(
    'hms_default_formatter' => array(
      'label' => t('Hours Minutes and Seconds'),
      'field types' => array(
        'hms_field',
      ),
      'settings' => array(
        'format' => 'h:mm',
        'leading_zero' => TRUE,
      ),
    ),
    'hms_natural_language_formatter' => array(
      'label' => t('Natural language'),
      'field types' => array(
        'hms_field',
      ),
      'settings' => array(
        'display_formats' => array(
          'w',
          'd',
          'h',
          'm',
          's',
        ),
        'separator' => ', ',
        'last_separator' => ' and ',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function hms_field_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = '';
  if ($display['type'] == 'hms_default_formatter') {
    $summary .= t('Format: @format', array(
      '@format' => $settings['format'],
    ));
    $summary .= '<br>';
    $summary .= t('Leading zero: @zero', array(
      '@zero' => $settings['leading_zero'] ? t('On') : t('Off'),
    ));
  }
  elseif ($display['type'] == 'hms_natural_language_formatter') {
    $factors = _hms_factor_map(TRUE);
    $fragments = $settings['display_formats'];
    $fragment_list = array();
    foreach ($fragments as $fragment) {
      if ($fragment) {
        $fragment_list[] = t($factors[$fragment]['label multiple']);
      }
    }
    $summary .= t('Displays: @display', array(
      '@display' => implode(', ', $fragment_list),
    ));
    $summary .= '<br>';
    $summary .= t('Separator: \'@separator\'', array(
      '@separator' => $settings['separator'],
    ));
    if (strlen($settings['last_separator'])) {
      $summary .= '<br>';
      $summary .= t('Last Separator: \'@last_separator\'', array(
        '@last_separator' => $settings['last_separator'],
      ));
    }
  }
  return $summary;
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function hms_field_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] == 'hms_default_formatter') {
    $element['format'] = array(
      '#type' => 'select',
      '#title' => t('Display format'),
      '#options' => _hms_format_options(),
      '#description' => t('The display format used for this field'),
      '#default_value' => $settings['format'],
      '#required' => TRUE,
    );
    $element['leading_zero'] = array(
      '#type' => 'checkbox',
      '#title' => t('Leading zero'),
      '#description' => t('Leading zero values will be displayed when this option is checked'),
      '#default_value' => $settings['leading_zero'],
    );
  }
  elseif ($display['type'] == 'hms_natural_language_formatter') {
    $options = array();
    $factors = _hms_factor_map(TRUE);
    $order = _hms_factor_map();
    arsort($order, SORT_NUMERIC);
    foreach ($order as $factor => $info) {
      $options[$factor] = t($factors[$factor]['label multiple']);
    }
    $element['display_formats'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Display fragments'),
      '#options' => $options,
      '#description' => t('Formats that are displayed in this field'),
      '#default_value' => $settings['display_formats'],
      '#required' => TRUE,
    );
    $element['separator'] = array(
      '#type' => 'textfield',
      '#title' => t('Separator'),
      '#description' => t('Separator used between fragments'),
      '#default_value' => $settings['separator'],
      '#required' => TRUE,
    );
    $element['last_separator'] = array(
      '#type' => 'textfield',
      '#title' => t('Last separator'),
      '#description' => t('Separator used between the last 2 fragments'),
      '#default_value' => $settings['last_separator'],
      '#required' => FALSE,
    );
  }
  return $element;
}

/**
 * Implements hook_field_formatter_view().
 */
function hms_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  if ($display['type'] === 'hms_default_formatter') {
    foreach ($items as $delta => $item) {
      $element[$delta]['#theme'] = 'hms';
      $element[$delta]['#value'] = $item['value'];
      $element[$delta]['#format'] = $display['settings']['format'];
      $element[$delta]['#leading_zero'] = $display['settings']['leading_zero'];
    }
  }
  else {
    if ($display['type'] === 'hms_natural_language_formatter') {
      foreach ($items as $delta => $item) {
        $element[$delta]['#theme'] = 'hms_natural_language';
        $element[$delta]['#value'] = $item['value'];
        $element[$delta]['#format'] = '';
        foreach ($display['settings']['display_formats'] as $fragment) {
          if ($fragment) {
            $element[$delta]['#format'] .= ':' . $fragment;
          }
        }
        if (!strlen($element[$delta]['#format'])) {
          $element[$delta]['#format'] = implode(':', array_keys($display['settings']['display_formats']));
        }
        else {
          $element[$delta]['#format'] = substr($element[$delta]['#format'], 1);
        }
        $element[$delta]['#separator'] = $display['settings']['separator'];
        $element[$delta]['#last_separator'] = $display['settings']['last_separator'];
      }
    }
  }
  return $element;
}

/**
 * Implements hook_theme().
 */
function hms_field_theme($existing, $type, $theme, $path) {
  return array(
    'hms' => array(
      'variables' => array(
        'value' => 0,
        'format' => 'h:mm',
        'leading_zero' => TRUE,
        'running_since' => 0,
        'offset' => 0,
        'default_value' => 0,
      ),
    ),
    'hms_natural_language' => array(
      'variables' => array(
        'value' => 0,
        'format' => 'w:d:h:m:s',
        'separator' => ', ',
        'last_separator' => ' and ',
      ),
    ),
  );
}

/**
 * Theme HMS
 */
function theme_hms($variables) {
  $classes = array(
    'hms',
    str_replace(':', '-', 'hms-format-' . $variables['format']),
  );
  $value = $variables['value'];
  $offset = $variables['offset'];
  $default_value = $variables['default_value'];
  if ($variables['running_since'] !== 0) {
    if (!$offset && !$default_value && $value) {

      // Backwards compatible.
      $offset = $value;
      $default_value = $value;
      $value = 0;
    }
    $value = $default_value;

    // It is not possible to run longer then from 1970-01-01 00:00:01
    $classes[] = 'hms-running';

    // We also need to pass the running since value to JS.
    // When format h is presented, the underlaying value can be at 3599
    // The next second h needs to update.
    // Be sure to pass running_since as time() (== GMT time)
    if ($variables['running_since'] < 0) {
      $variables['running_since'] = REQUEST_TIME;
    }
    $classes[] = 'hms-since-' . $variables['running_since'];
    $classes[] = 'hms-offset-' . $offset;
    $classes[] = 'hms-leading_zero-' . $variables['leading_zero'];
    if ($offset) {
      $value = REQUEST_TIME - $variables['running_since'] + $offset;
    }
    _hms_add_running_js();
  }
  $html = '<span class="' . implode(' ', $classes) . '">';
  $html .= _hms_seconds_to_formatted($value, $variables['format'], $variables['leading_zero']);
  $html .= '</span>';
  return $html;
}

/**
 * Theme hms_natural_language
 *
 * TODO: Investigate running since options (see theme_hms)
 *   Would be cool if we can also make this format a 'Forrest Gump' format.
 */
function theme_hms_natural_language($variables) {
  $labels = _hms_factor_map(TRUE);

  // Assign keyed values array.
  $values = array_combine(explode(':', $variables['format']), explode(':', _hms_seconds_to_formatted($variables['value'], $variables['format'], TRUE)));

  // Spit out HTML per value (only when value > 0).
  $html = array();
  foreach ($values as $key => $val) {
    if ($val != 0) {
      $html[] = '<span class="' . drupal_clean_css_identifier($labels[$key]['label multiple']) . '">' . format_plural($val, '@count ' . $labels[$key]['label single'], '@count ' . $labels[$key]['label multiple']) . '</span>';
    }
  }

  // Serial commas - and
  $and = $comma = t($variables['separator']);
  if (isset($variables['last_separator']) && strlen($variables['last_separator'])) {
    $and = t($variables['last_separator']);
  }
  switch (count($html)) {
    case 0:
    case 1:
      return reset($html);
    default:
      $last = array_pop($html);
      return implode($comma, $html) . $and . $last;
  }
}

/**
 * Helpers.
 */

/**
 * HMS form element validator.
 */
function _hms_validate_form_element($element, &$form_state, $form) {
  $form_input = drupal_array_get_nested_value($form_state['input'], $element['#array_parents']);
  if (_hms_formatted_to_seconds($form_input, $element['#format']) === FALSE) {
    form_error($element, t('@title field cannot be parsed. Please use format \'%format\'.', array(
      '@title' => $element['#title'],
      '%format' => $element['#format'],
    )));
  }
}

/**
* Returns possible format options.
*/
function _hms_format_options() {
  $format = drupal_static(__FUNCTION__);
  if (empty($format)) {
    $format = array(
      'ISO 8601 based' => array(
        'h:mm' => 'h:mm',
        'h:mm:ss' => 'h:mm:ss',
        'm:ss' => 'm:ss',
        'h' => 'h',
        'm' => 'm',
        's' => 's',
      ),
      'Space separated' => array(
        'hms' => 'e.q. 3h 15m 30s',
      ),
    );
    drupal_alter('hms_format', $format);
  }
  return $format;
}

/**
 * Returns the factor map of the format options.
 *
 * Note: We cannot go further then weeks in this setup.
 *       A month implies that we know how many seconds a month is.
 *       Problem here is that a month can be 28(29), 30 or 31 days.
 *       Same goes for C (century) Y (year) Q (quarter).
 *       Only solution is that we have a value relative to a date.
 *
 *  Use HOOK_hms_factor_alter($factors) to do your own magic.
 */
function _hms_factor_map($return_full = FALSE) {
  $factor = drupal_static(__FUNCTION__);
  if (empty($factor)) {
    $factor = array(
      'w' => array(
        'factor value' => 604800,
        'label single' => t('week'),
        'label multiple' => t('weeks'),
      ),
      'd' => array(
        'factor value' => 86400,
        'label single' => t('day'),
        'label multiple' => t('days'),
      ),
      'h' => array(
        'factor value' => 3600,
        'label single' => t('hour'),
        'label multiple' => t('hours'),
      ),
      'm' => array(
        'factor value' => 60,
        'label single' => t('minute'),
        'label multiple' => t('minutes'),
      ),
      's' => array(
        'factor value' => 1,
        'label single' => t('second'),
        'label multiple' => t('seconds'),
      ),
    );
    drupal_alter('hms_factor', $factor);
  }
  if ($return_full) {
    return $factor;
  }

  // We only return the factor value here.
  // for historical reasons we also check if value is an array.
  $return = array();
  foreach ($factor as $key => $val) {
    $value = is_array($val) ? $val['factor value'] : $val;
    $return[$key] = $value;
  }
  return $return;
}

/**
 * Returns number of seconds from a formatted string.
 *
 * NULL is empty value
 * 0 is 0
 * FALSE is error.
 */
function _hms_formatted_to_seconds($str, $format = 'h:m:s', $element = NULL) {
  if (!strlen($str)) {
    return NULL;
  }
  if ($str == '0') {
    return 0;
  }
  $value = 0;
  $error = FALSE;

  // Input validation for space separated format.
  if ($format == 'hms') {
    $preg = array();
    if (is_numeric($str) || preg_match('/^(?P<H>[-]{0,1}[0-9]{1,5}(\\.[0-9]{1,3})?)$|^(?P<negative>[-]{0,1})(((?P<w>[0-9.]{1,5})w)?((?P<d>[0-9.]{1,5})d)?((?P<h>[0-9.]{1,5})h)?([ ]{0,1})((?P<m>[0-9.]{1,05})m)?([ ]{0,1})((?P<s>[0-9.]{1,5})s)?)/', $str, $preg)) {
      $error = TRUE;
      foreach ($preg as $code => $val) {
        if (!is_numeric($val)) {
          continue;
        }
        switch ($code) {
          case 'w':
            $error = FALSE;
            $value += $val * 604800;
            break;
          case 'd':
            $error = FALSE;
            $value += $val * 86400;
            break;
          case 'h':
          case 'H':
            $error = FALSE;
            $value += $val * 3600;
            break;
          case 'm':
            $error = FALSE;
            $value += $val * 60;
            break;
          case 's':
            $error = FALSE;
            $value += $val;
            break;
          default:
            break;
        }
      }
      if (!empty($preg['negative'])) {
        $value = $value * -1;
      }
      if ($error == 0) {
        return $value;
      }
    }
    else {
      $error = TRUE;
    }
  }

  // Input validation ISO 8601 based.
  $preg_string = preg_replace(array(
    '/[h]{1,6}/',
    '/[m]{1,2}|[s]{1,2}/',
  ), array(
    '([0-9]{1,6})',
    '([0-9]{1,2})',
  ), $format);
  if (!preg_match("@^" . $preg_string . "\$@", $str) && !preg_match('/^[0-9]{1,6}([,.][0-9]{1,6})?$/', $str)) {
    $error = TRUE;
  }

  // Does not follow space separated format.
  if ($error) {
    if (!empty($element)) {
      form_error($element, t('The "!name" value is in wrong format, check in field settings.', array(
        '!name' => t($element['#title']),
      )));
    }
    else {
      form_set_error(NULL, t('The "!name" value is in wrong format, check in field settings.'));
    }
    return FALSE;
  }

  // is the value negative?
  $negative = FALSE;
  if (substr($str, 0, 1) == '-') {
    $negative = TRUE;
    $str = substr($str, 1);
  }
  $factor_map = _hms_factor_map();
  $search = _hms_normalize_format($format);
  for ($i = 0; $i < strlen($search); $i++) {

    // Is this char in the factor map?
    if (isset($factor_map[$search[$i]])) {
      $factor = $factor_map[$search[$i]];

      // What is the next seperator to search for?
      $bumper = '$';
      if (isset($search[$i + 1])) {
        $bumper = '(' . preg_quote($search[$i + 1], '/') . '|$)';
      }
      if (preg_match_all('/^(.*)' . $bumper . '/U', $str, $matches)) {

        // Replace , with .
        $num = str_replace(',', '.', $matches[1][0]);

        // Return error when found string is not numeric
        if (!is_numeric($num)) {
          return FALSE;
        }

        // Shorten $str
        $str = substr($str, strlen($matches[1][0]));

        // Calculate value
        $value += $num * $factor;
      }
    }
    elseif (substr($str, 0, 1) == $search[$i]) {

      // Expected this value, cut off and go ahead.
      $str = substr($str, 1);
    }
    else {

      // Does not follow format.
      return FALSE;
    }
    if (!strlen($str)) {

      // No more $str to investigate.
      break;
    }
  }
  if ($negative) {
    $value = 0 - $value;
  }
  return $value;
}

/**
 * Returns a formatted string form the number of seconds.
 */
function _hms_seconds_to_formatted($seconds, $format = 'h:mm', $leading_zero = TRUE) {

  // Return NULL on empty string.
  if ($seconds === '') {
    return NULL;
  }
  $factor = _hms_factor_map();

  // We need factors, biggest first.
  arsort($factor, SORT_NUMERIC);
  $values = array();
  $left_over = $seconds;
  $str = '';
  if ($seconds < 0) {
    $str .= '-';
    $left_over = abs($left_over);
  }

  // Space separated format
  if ($format == 'hms') {
    foreach ($factor as $key => $val) {
      if ($left_over == 0) {
        break;
      }
      $values[$key] = floor($left_over / $factor[$key]);
      if ($values[$key]) {
        $left_over -= $values[$key] * $factor[$key];
        $str .= $values[$key] . $key . ' ';
      }
    }
  }
  else {
    foreach ($factor as $key => $val) {
      if (strpos($format, $key) === FALSE) {
        continue;

        // Not in our format, please go on, so we can plus this on a value in our format.
      }
      if ($left_over == 0) {
        $values[$key] = 0;
        continue;
      }
      $values[$key] = floor($left_over / $factor[$key]);
      $left_over -= $values[$key] * $factor[$key];
    }
    $format = explode(':', $format);
    foreach ($format as $key) {
      if (!$leading_zero && (empty($values[substr($key, 0, 1)]) || !$values[substr($key, 0, 1)])) {
        continue;
      }
      $leading_zero = TRUE;
      $str .= sprintf('%0' . strlen($key) . 'd', $values[substr($key, 0, 1)]) . ':';
    }
    if (!strlen($str)) {
      $key = array_pop($format);
      $str = sprintf('%0' . strlen($key) . 'd', 0) . ':';
    }
  }
  return substr($str, 0, -1);
}

/**
 * Helper to normalize format.
 *
 * Changes double keys to single keys.
 */
function _hms_normalize_format($format) {
  $keys = array_keys(_hms_factor_map());
  $search_keys = array_map('_add_multi_search_tokens', $keys);
  return preg_replace($search_keys, $keys, $format);
}

/**
 * Helper to extend values in search array
 */
function _add_multi_search_tokens($item) {
  return '/' . $item . '+/';
}

/**
 * Helper function to convert input values to seconds (FORM API).
 */
function _hms_value_callback($element, $input = NULL, $form_state) {
  if ($form_state['process_input'] && (!empty($input) || $input === 0)) {
    $seconds = _hms_formatted_to_seconds($input, $element['#format']);

    // If seconds are 0 return string instead of integer.
    if ($seconds == 0) {
      return '0';
    }
    return $seconds;
  }
  return $input;
}

/**
 * Helper function to convert seconds to a formatted value (FORM API).
 */
function _hms_pre_render_form_element($element) {
  $value = $element['#value'] !== FALSE ? $element['#value'] : $element['#default_value'];
  if ((!empty($value) || (int) $value === 0) && is_numeric($value)) {
    $element['#value'] = _hms_seconds_to_formatted($value, $element['#format']);
  }
  return $element;
}

/**
 * Add js for running HMS fields.
 */
function _hms_add_running_js() {
  $hms_running_js_added =& drupal_static(__FUNCTION__);
  if (!empty($hms_running_js_added)) {
    return;
  }
  $hms_running_js_added = TRUE;
  drupal_add_js(drupal_get_path('module', 'hms_field') . '/hms_field.js');
  drupal_add_js(array(
    'hms_field' => array(
      'servertime' => REQUEST_TIME,
      'factor_map' => _hms_factor_map(),
    ),
  ), 'setting');
}

Functions

Namesort descending Description
hms_field_element_info Implements hook_element_info().
hms_field_field_formatter_info Implements hook_field_formatter_info().
hms_field_field_formatter_settings_form Implements hook_field_formatter_settings_form().
hms_field_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
hms_field_field_formatter_view Implements hook_field_formatter_view().
hms_field_field_info Implements hook_field_info().
hms_field_field_instance_settings_form Implements hook_field_instance_settings_form().
hms_field_field_is_empty Implements hook_field_is_empty().
hms_field_field_widget_form Implements hook_field_widget_form().
hms_field_field_widget_info Implements hook_field_widget_info().
hms_field_theme Implements hook_theme().
hms_field_views_api Implements hook_views_api().
theme_hms Theme HMS
theme_hms_natural_language Theme hms_natural_language
_add_multi_search_tokens Helper to extend values in search array
_hms_add_running_js Add js for running HMS fields.
_hms_factor_map Returns the factor map of the format options.
_hms_formatted_to_seconds Returns number of seconds from a formatted string.
_hms_format_options Returns possible format options.
_hms_normalize_format Helper to normalize format.
_hms_pre_render_form_element Helper function to convert seconds to a formatted value (FORM API).
_hms_seconds_to_formatted Returns a formatted string form the number of seconds.
_hms_validate_form_element HMS form element validator.
_hms_value_callback Helper function to convert input values to seconds (FORM API).