number.inc 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. <?php
  2. /**
  3. * @file
  4. * Webform module number component.
  5. */
  6. /**
  7. * Implements _webform_defaults_component().
  8. */
  9. function _webform_defaults_number() {
  10. return array(
  11. 'name' => '',
  12. 'form_key' => NULL,
  13. 'pid' => 0,
  14. 'weight' => 0,
  15. 'value' => '',
  16. 'required' => 0,
  17. 'extra' => array(
  18. 'type' => 'textfield',
  19. 'field_prefix' => '',
  20. 'field_suffix' => '',
  21. 'disabled' => 0,
  22. 'unique' => 0,
  23. 'title_display' => 0,
  24. 'description' => '',
  25. 'description_above' => FALSE,
  26. 'placeholder' => '',
  27. 'attributes' => array(),
  28. 'private' => FALSE,
  29. 'analysis' => FALSE,
  30. 'min' => '',
  31. 'max' => '',
  32. 'step' => '',
  33. 'decimals' => '',
  34. 'point' => '.',
  35. 'separator' => ',',
  36. 'integer' => 0,
  37. 'excludezero' => 0,
  38. ),
  39. );
  40. }
  41. /**
  42. * Implements _webform_theme_component().
  43. */
  44. function _webform_theme_number() {
  45. return array(
  46. 'webform_number' => array(
  47. 'render element' => 'element',
  48. 'file' => 'components/number.inc',
  49. ),
  50. 'webform_display_number' => array(
  51. 'render element' => 'element',
  52. 'file' => 'components/number.inc',
  53. ),
  54. );
  55. }
  56. /**
  57. * Fix the view field(s) that are automatically generated for number components.
  58. */
  59. function _webform_view_field_number($component, $fields) {
  60. foreach ($fields as &$field) {
  61. $field['webform_datatype'] = 'number';
  62. }
  63. return $fields;
  64. }
  65. /**
  66. * Implements _webform_edit_component().
  67. */
  68. function _webform_edit_number($component) {
  69. $form = array();
  70. $form['value'] = array(
  71. '#type' => 'textfield',
  72. '#title' => t('Default value'),
  73. '#default_value' => $component['value'],
  74. '#description' => t('The default value of the field.') . ' ' . theme('webform_token_help'),
  75. '#size' => 60,
  76. '#maxlength' => 1024,
  77. '#weight' => 0,
  78. );
  79. $form['display']['type'] = array(
  80. '#type' => 'radios',
  81. '#title' => t('Element type'),
  82. '#options' => array(
  83. 'textfield' => t('Text field'),
  84. 'select' => t('Select list'),
  85. ),
  86. '#default_value' => $component['extra']['type'],
  87. '#description' => t('A minimum and maximum value are required if displaying as a select.'),
  88. '#weight' => -1,
  89. '#parents' => array('extra', 'type'),
  90. );
  91. $form['display']['placeholder'] = array(
  92. '#type' => 'textfield',
  93. '#title' => t('Placeholder'),
  94. '#default_value' => $component['extra']['placeholder'],
  95. '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'),
  96. '#weight' => 1,
  97. '#parents' => array('extra', 'placeholder'),
  98. );
  99. $form['display']['field_prefix'] = array(
  100. '#type' => 'textfield',
  101. '#title' => t('Prefix text placed to the left of the field'),
  102. '#default_value' => $component['extra']['field_prefix'],
  103. '#description' => t('Examples: $, #, -.'),
  104. '#size' => 20,
  105. '#maxlength' => 127,
  106. '#weight' => 1.1,
  107. '#parents' => array('extra', 'field_prefix'),
  108. );
  109. $form['display']['field_suffix'] = array(
  110. '#type' => 'textfield',
  111. '#title' => t('Postfix text placed to the right of the field'),
  112. '#default_value' => $component['extra']['field_suffix'],
  113. '#description' => t('Examples: lb, kg, %.'),
  114. '#size' => 20,
  115. '#maxlength' => 127,
  116. '#weight' => 1.2,
  117. '#parents' => array('extra', 'field_suffix'),
  118. );
  119. $form['display']['disabled'] = array(
  120. '#type' => 'checkbox',
  121. '#title' => t('Disabled'),
  122. '#return_value' => 1,
  123. '#description' => t('Make this field non-editable. Useful for displaying default value. Changeable via JavaScript or developer tools.'),
  124. '#weight' => 11,
  125. '#default_value' => $component['extra']['disabled'],
  126. '#parents' => array('extra', 'disabled'),
  127. );
  128. $form['display']['decimals'] = array(
  129. '#type' => 'select',
  130. '#title' => t('Decimal places'),
  131. '#options' => array('' => t('Automatic')) + drupal_map_assoc(range(0, 10)),
  132. '#description' => t('Automatic will display up to @count decimals places if needed. A value of "2" is common to format currency amounts.', array('@count' => '4')),
  133. '#default_value' => $component['extra']['decimals'],
  134. '#weight' => 2,
  135. '#parents' => array('extra', 'decimals'),
  136. '#element_validate' => array('_webform_edit_number_validate'),
  137. );
  138. $form['display']['separator'] = array(
  139. '#type' => 'select',
  140. '#title' => t('Thousands separator'),
  141. '#options' => array(
  142. ',' => t('Comma (,)'),
  143. '.' => t('Period (.)'),
  144. ' ' => t('Space ( )'),
  145. '' => t('None'),
  146. ),
  147. '#default_value' => $component['extra']['separator'],
  148. '#weight' => 3,
  149. '#parents' => array('extra', 'separator'),
  150. '#element_validate' => array('_webform_edit_number_validate'),
  151. );
  152. $form['display']['point'] = array(
  153. '#type' => 'select',
  154. '#title' => t('Decimal point'),
  155. '#options' => array(
  156. ',' => t('Comma (,)'),
  157. '.' => t('Period (.)'),
  158. ),
  159. '#default_value' => $component['extra']['point'],
  160. '#weight' => 4,
  161. '#parents' => array('extra', 'point'),
  162. '#element_validate' => array('_webform_edit_number_validate'),
  163. );
  164. $form['validation']['unique'] = array(
  165. '#type' => 'checkbox',
  166. '#title' => t('Unique'),
  167. '#return_value' => 1,
  168. '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
  169. '#weight' => 1,
  170. '#default_value' => $component['extra']['unique'],
  171. '#parents' => array('extra', 'unique'),
  172. );
  173. $form['validation']['integer'] = array(
  174. '#type' => 'checkbox',
  175. '#title' => t('Integer'),
  176. '#return_value' => 1,
  177. '#description' => t('Permit only integer values as input. For example, 12.34 would be invalid.'),
  178. '#weight' => 1.5,
  179. '#default_value' => $component['extra']['integer'],
  180. '#parents' => array('extra', 'integer'),
  181. );
  182. $form['validation']['min'] = array(
  183. '#type' => 'textfield',
  184. '#title' => t('Minimum'),
  185. '#default_value' => $component['extra']['min'],
  186. '#description' => t('Minimum numeric value. For example, 0 would ensure positive numbers.'),
  187. '#size' => 5,
  188. '#maxlength' => 10,
  189. '#weight' => 2.1,
  190. '#parents' => array('extra', 'min'),
  191. '#element_validate' => array('_webform_edit_number_validate'),
  192. );
  193. $form['validation']['max'] = array(
  194. '#type' => 'textfield',
  195. '#title' => t('Maximum'),
  196. '#default_value' => $component['extra']['max'],
  197. '#description' => t('Maximum numeric value. This may also determine the display width of your field.'),
  198. '#size' => 5,
  199. '#maxlength' => 10,
  200. '#weight' => 2.2,
  201. '#parents' => array('extra', 'max'),
  202. '#element_validate' => array('_webform_edit_number_validate'),
  203. );
  204. $form['validation']['step'] = array(
  205. '#type' => 'textfield',
  206. '#title' => t('Step'),
  207. '#default_value' => $component['extra']['step'],
  208. '#description' => t('Limit options to a specific increment. For example, a step of "5" would allow values 5, 10, 15, etc.'),
  209. '#size' => 5,
  210. '#maxlength' => 10,
  211. '#weight' => 3,
  212. '#parents' => array('extra', 'step'),
  213. '#element_validate' => array('_webform_edit_number_validate'),
  214. );
  215. // Analysis settings.
  216. $form['analysis'] = array(
  217. '#type' => 'fieldset',
  218. '#title' => t('Analysis'),
  219. '#collapsible' => TRUE,
  220. '#collapsed' => FALSE,
  221. '#weight' => 10,
  222. );
  223. $form['analysis']['excludezero'] = array(
  224. '#type' => 'checkbox',
  225. '#title' => t('Exclude zero'),
  226. '#return_value' => 1,
  227. '#description' => t('Exclude entries of zero (or blank) when counting submissions to calculate average and standard deviation.'),
  228. '#weight' => 1.5,
  229. '#default_value' => $component['extra']['excludezero'],
  230. '#parents' => array('extra', 'excludezero'),
  231. );
  232. return $form;
  233. }
  234. /**
  235. * Theme function to render a number component.
  236. */
  237. function theme_webform_number($variables) {
  238. $element = $variables['element'];
  239. // This IF statement is mostly in place to allow our tests to set type="text"
  240. // because SimpleTest does not support type="number".
  241. if (!isset($element['#attributes']['type'])) {
  242. // HTML5 number fields are no long used pending better browser support.
  243. // See issues #2290029, #2202905.
  244. // @code
  245. // $element['#attributes']['type'] = 'number';
  246. // @endcode
  247. $element['#attributes']['type'] = 'text';
  248. }
  249. // Step property *must* be a full number with 0 prefix if a decimal.
  250. if (!empty($element['#step']) && filter_var((float) $element['#step'], FILTER_VALIDATE_INT) === FALSE) {
  251. $decimals = strlen($element['#step']) - strrpos($element['#step'], '.') - 1;
  252. $element['#step'] = sprintf('%1.' . $decimals . 'F', $element['#step']);
  253. }
  254. // If the number is not an integer and step is undefined/empty, set the "any"
  255. // value to allow any decimal.
  256. if (empty($element['#integer']) && empty($element['#step'])) {
  257. $element['#step'] = 'any';
  258. }
  259. elseif ($element['#integer'] && empty($element['#step'])) {
  260. $element['#step'] = 1;
  261. }
  262. // Convert properties to attributes on the element if set.
  263. foreach (array('id', 'name', 'value', 'size', 'min', 'max', 'step') as $property) {
  264. if (isset($element['#' . $property]) && $element['#' . $property] !== '') {
  265. $element['#attributes'][$property] = $element['#' . $property];
  266. }
  267. }
  268. _form_set_class($element, array('form-text', 'form-number'));
  269. return '<input' . drupal_attributes($element['#attributes']) . ' />';
  270. }
  271. /**
  272. * Implements _webform_render_component().
  273. */
  274. function _webform_render_number($component, $value = NULL, $filter = TRUE, $submission = NULL) {
  275. $node = isset($component['nid']) ? node_load($component['nid']) : NULL;
  276. $element = array(
  277. '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'],
  278. '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
  279. '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'],
  280. '#required' => $component['required'],
  281. '#weight' => $component['weight'],
  282. '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']),
  283. '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']),
  284. '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'],
  285. '#attributes' => $component['extra']['attributes'],
  286. '#element_validate' => array('_webform_validate_number'),
  287. '#theme_wrappers' => array('webform_element'),
  288. '#min' => $component['extra']['min'],
  289. '#max' => $component['extra']['max'],
  290. '#step' => $component['extra']['step'] ? abs($component['extra']['step']) : '',
  291. '#integer' => $component['extra']['integer'],
  292. '#point' => $component['extra']['point'],
  293. '#separator' => $component['extra']['separator'],
  294. '#decimals' => $component['extra']['decimals'],
  295. '#translatable' => array('title', 'description', 'field_prefix', 'field_suffix', 'placeholder'),
  296. );
  297. if ($component['required']) {
  298. $element['#attributes']['required'] = 'required';
  299. }
  300. if ($component['extra']['placeholder']) {
  301. $element['#attributes']['placeholder'] = $component['extra']['placeholder'];
  302. }
  303. // Set the decimal count to zero for integers.
  304. if ($element['#integer'] && $element['#decimals'] === '') {
  305. $element['#decimals'] = 0;
  306. }
  307. // Flip the min and max properties to make min less than max if needed.
  308. if ($element['#min'] !== '' && $element['#max'] !== '' && $element['#min'] > $element['#max']) {
  309. $max = $element['#min'];
  310. $element['#min'] = $element['#max'];
  311. $element['#max'] = $max;
  312. }
  313. // Ensure #step starts with a zero if a decimal.
  314. if (filter_var((float) $element['#step'], FILTER_VALIDATE_INT) === FALSE) {
  315. $decimals = strlen($element['#step']) - strrpos($element['#step'], '.') - 1;
  316. $element['#step'] = sprintf('%1.' . $decimals . 'F', $element['#step']);
  317. }
  318. if ($component['extra']['type'] == 'textfield') {
  319. // Render as textfield.
  320. $element['#type'] = 'webform_number';
  321. // Set the size property based on #max, to ensure consistent behavior for
  322. // browsers that do not support type = number.
  323. if ($element['#max']) {
  324. $element['#size'] = strlen($element['#max']) + 1;
  325. }
  326. }
  327. else {
  328. // Render as select.
  329. $element['#type'] = 'select';
  330. // Create user-specified options list as an array.
  331. $element['#options'] = _webform_number_select_options($component);
  332. // Add default options if using a select list with no default. This trigger's
  333. // Drupal 7's adding of the option for us. See form_process_select().
  334. if ($component['extra']['type'] == 'select' && $element['#default_value'] === '') {
  335. $element['#empty_value'] = '';
  336. }
  337. }
  338. // Set user-entered values.
  339. if (isset($value[0])) {
  340. // If the value has been standardized, convert it to the expected format
  341. // for display to the user.
  342. if (webform_number_format_match($value[0], '.', '')) {
  343. $element['#default_value'] = _webform_number_format($component, $value[0]);
  344. }
  345. // Otherwise use the user-defined input.
  346. else {
  347. $element['#default_value'] = $value[0];
  348. }
  349. }
  350. // Enforce uniqueness.
  351. if ($component['extra']['unique']) {
  352. $element['#element_validate'][] = 'webform_validate_unique';
  353. }
  354. // Set readonly if disabled.
  355. if ($component['extra']['disabled']) {
  356. if ($filter) {
  357. $element['#attributes']['readonly'] = 'readonly';
  358. }
  359. else {
  360. $element['#disabled'] = TRUE;
  361. }
  362. }
  363. return $element;
  364. }
  365. /**
  366. * Implements _webform_display_component().
  367. */
  368. function _webform_display_number($component, $value, $format = 'html', $submission = array()) {
  369. $empty = !isset($value[0]) || $value[0] === '';
  370. return array(
  371. '#title' => $component['name'],
  372. '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
  373. '#weight' => $component['weight'],
  374. '#theme' => 'webform_display_number',
  375. '#theme_wrappers' => $format == 'html' ? array('webform_element') : array('webform_element_text'),
  376. '#field_prefix' => $empty ? '' : $component['extra']['field_prefix'],
  377. '#field_suffix' => $empty ? '' : $component['extra']['field_suffix'],
  378. '#format' => $format,
  379. '#value' => $empty ? '' : _webform_number_format($component, $value[0]),
  380. '#translatable' => array('title', 'placeholder'),
  381. );
  382. }
  383. /**
  384. * Format the output of data for this component.
  385. */
  386. function theme_webform_display_number($variables) {
  387. $element = $variables['element'];
  388. $prefix = $element['#format'] == 'html' ? '' : $element['#field_prefix'];
  389. $suffix = $element['#format'] == 'html' ? '' : $element['#field_suffix'];
  390. $value = $element['#format'] == 'html' ? check_plain($element['#value']) : $element['#value'];
  391. return $value !== '' ? ($prefix . $value . $suffix) : ' ';
  392. }
  393. /**
  394. * Implements _webform_analysis_component().
  395. */
  396. function _webform_analysis_number($component, $sids = array(), $single = FALSE, $join = NULL) {
  397. $advanced_stats = $single;
  398. $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC))
  399. ->fields('wsd', array('data'))
  400. ->condition('wsd.nid', $component['nid'])
  401. ->condition('wsd.cid', $component['cid']);
  402. if (count($sids)) {
  403. $query->condition('wsd.sid', $sids, 'IN');
  404. }
  405. if ($join) {
  406. $query->innerJoin($join, 'ws2_', 'wsd.sid = ws2_.sid');
  407. }
  408. $population = array();
  409. $submissions = 0;
  410. $non_zero = 0;
  411. $non_empty = 0;
  412. $sum = 0;
  413. $result = $query->execute();
  414. foreach ($result as $data) {
  415. $value = trim($data['data']);
  416. $number = (float) $value;
  417. $non_empty += (integer) ($value !== '');
  418. $non_zero += (integer) ($number != 0.0);
  419. $sum += $number;
  420. $population[] = $number;
  421. $submissions++;
  422. }
  423. sort($population, SORT_NUMERIC);
  424. // Average and population count.
  425. if ($component['extra']['excludezero']) {
  426. $average = $non_zero ? ($sum / $non_zero) : 0;
  427. $average_title = t('Average !mu excluding zeros/blanks', array('!mu' => $advanced_stats ? '(&mu;)' : ''));
  428. // Sample (sub-set of total population).
  429. $population_count = $non_zero - 1;
  430. $sigma = 'sd';
  431. $description = t('sample');
  432. }
  433. else {
  434. $average = $submissions ? ($sum / $submissions) : 0;
  435. $average_title = t('Average !mu including zeros/blanks', array('!mu' => $advanced_stats ? '(&mu;)' : ''));
  436. // Population.
  437. $population_count = $submissions;
  438. $sigma = '&sigma;';
  439. $description = t('population');
  440. }
  441. // Formatting.
  442. $average = _webform_number_format($component, $average);
  443. $sum = _webform_number_format($component, $sum);
  444. $rows[] = array(t('Zero/blank'), ($submissions - $non_zero));
  445. $rows[] = array(t('User entered value'), $non_empty);
  446. $other[] = array(t('Sum') . ($advanced_stats ? ' (&Sigma;)' : ''), $sum);
  447. $other[] = array($average_title, $average);
  448. if (!$advanced_stats && $sum != 0) {
  449. $other[] = l(t('More stats »'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']);
  450. }
  451. // Normal distribution information.
  452. if ($advanced_stats && $population_count && $sum != 0) {
  453. // Standard deviation.
  454. $stddev = 0;
  455. foreach ($population as $value) {
  456. // Obtain the total of squared variances.
  457. $stddev += pow(($value - $average), 2);
  458. }
  459. if ($population_count > 0) {
  460. $stddev = sqrt($stddev / $population_count);
  461. }
  462. else {
  463. $stddev = sqrt($stddev);
  464. }
  465. // Skip the rest of the distribution rows if standard deviation is 0.
  466. if (empty($stddev)) {
  467. return array(
  468. 'table_rows' => $rows,
  469. 'other_data' => $other,
  470. );
  471. }
  472. // Build normal distribution table rows.
  473. $count = array();
  474. $percent = array();
  475. $limit = array();
  476. $index = 0;
  477. $count[] = 0;
  478. $limit[] = $average - ($stddev * 4);
  479. foreach ($population as $value) {
  480. while ($value >= $limit[$index]) {
  481. $percent[] = number_format($count[$index] / $population_count * 100, 2, '.', '');
  482. $limit[] = $limit[$index] + $stddev;
  483. $index += 1;
  484. if ($limit[$index] == $average) {
  485. $limit[$index] = $limit[$index] + $stddev;
  486. }
  487. $count[$index] = 0;
  488. }
  489. $count[$index] += 1;
  490. }
  491. $percent[] = number_format($count[$index] / $population_count * 100, 2, '.', '');
  492. // Format normal distribution table output.
  493. $stddev = _webform_number_format($component, $stddev);
  494. $low = _webform_number_format($component, $population[0]);
  495. $high = _webform_number_format($component, end($population));
  496. foreach ($limit as $key => $value) {
  497. $limit[$key] = _webform_number_format($component, $value);
  498. }
  499. // Column headings (override potential theme uppercase, for example, Seven in D7).
  500. $header = array(
  501. t('Normal Distribution'),
  502. array('data' => '-4' . $sigma, 'style' => 'text-transform: lowercase;'),
  503. array('data' => '-3' . $sigma, 'style' => 'text-transform: lowercase;'),
  504. array('data' => '-2' . $sigma, 'style' => 'text-transform: lowercase;'),
  505. array('data' => '-1' . $sigma, 'style' => 'text-transform: lowercase;'),
  506. array('data' => '+1' . $sigma, 'style' => 'text-transform: lowercase;'),
  507. array('data' => '+2' . $sigma, 'style' => 'text-transform: lowercase;'),
  508. array('data' => '+3' . $sigma, 'style' => 'text-transform: lowercase;'),
  509. array('data' => '+4' . $sigma, 'style' => 'text-transform: lowercase;'),
  510. );
  511. // Insert row labels.
  512. array_unshift($limit, t('Boundary'));
  513. array_unshift($count, t('Count'));
  514. array_unshift($percent, t('% of !description', array('!description' => $description)));
  515. $normal_distribution = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent), 'sticky' => FALSE));
  516. $other[] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high)));
  517. $other[] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev);
  518. $other[] = $normal_distribution;
  519. }
  520. return array(
  521. 'table_rows' => $rows,
  522. 'other_data' => $other,
  523. );
  524. }
  525. /**
  526. * Implements _webform_table_component().
  527. */
  528. function _webform_table_number($component, $value) {
  529. return isset($value[0]) ? _webform_number_format($component, $value[0]) : '';
  530. }
  531. /**
  532. * Implements _webform_action_set_component().
  533. */
  534. function _webform_action_set_number($component, &$element, &$form_state, $value) {
  535. $element['#value'] = $value;
  536. form_set_value($element, $value, $form_state);
  537. }
  538. /**
  539. * Implements _webform_csv_headers_component().
  540. */
  541. function _webform_csv_headers_number($component, $export_options) {
  542. $header = array();
  543. $header[0] = '';
  544. $header[1] = '';
  545. $header[2] = $export_options['header_keys'] ? $component['form_key'] : $component['name'];
  546. return $header;
  547. }
  548. /**
  549. * Implements _webform_csv_data_component().
  550. */
  551. function _webform_csv_data_number($component, $export_options, $value) {
  552. if (isset($value[0]) && is_numeric($value[0]) && $component['extra']['decimals'] !== '') {
  553. $value[0] = number_format($value[0], $component['extra']['decimals'], '.', '');
  554. }
  555. return isset($value[0]) ? $value[0] : '';
  556. }
  557. /**
  558. * A Drupal Form API Validation function.
  559. *
  560. * Validates the entered values from number components on the client-side form.
  561. *
  562. * @param array $element
  563. * The form element. May either be a select or a webform_number element.
  564. * @param array $form_state
  565. * The full form state for the webform.
  566. */
  567. function _webform_validate_number($element, &$form_state) {
  568. // Trim spaces for basic cleanup.
  569. $value = trim($element['#value']);
  570. form_set_value($element, $value, $form_state);
  571. if ($value != '') {
  572. // First check that the entered value matches the expected value.
  573. if (!webform_number_format_match($value, $element['#point'], $element['#separator'])) {
  574. form_error($element, t('!name field value must format numbers as "@example".', array('!name' => $element['#title'], '@example' => webform_number_format(12345.6789, $element['#decimals'], $element['#point'], $element['#separator']))));
  575. return;
  576. }
  577. // Numeric test.
  578. $numeric_value = webform_number_standardize($value, $element['#point']);
  579. if (is_numeric($numeric_value)) {
  580. // Range test.
  581. if ($element['#min'] != '' && $element['#max'] != '') {
  582. // Flip minimum and maximum if needed.
  583. if ($element['#max'] > $element['#min']) {
  584. $min = $element['#min'];
  585. $max = $element['#max'];
  586. }
  587. else {
  588. $min = $element['#max'];
  589. $max = $element['#min'];
  590. }
  591. if ($numeric_value > $max || $numeric_value < $min) {
  592. form_error($element, t('!name field value of @value should be in the range @min to @max.', array('!name' => $element['#title'], '@value' => $value, '@min' => $element['#min'], '@max' => $element['#max'])));
  593. }
  594. }
  595. elseif ($element['#max'] != '' && $numeric_value > $element['#max']) {
  596. form_error($element, t('!name field value must be less than @max.', array('!name' => $element['#title'], '@max' => $element['#max'])));
  597. }
  598. elseif ($element['#min'] != '' && $numeric_value < $element['#min']) {
  599. form_error($element, t('!name field value must be greater than @min.', array('!name' => $element['#title'], '@min' => $element['#min'])));
  600. }
  601. // Integer test.
  602. if ($element['#integer'] && filter_var((float) $numeric_value, FILTER_VALIDATE_INT) === FALSE) {
  603. form_error($element, t('!name field value of @value must be an integer.', array('!name' => $element['#title'], '@value' => $value)));
  604. }
  605. // Step test.
  606. $starting_number = $element['#min'] ? $element['#min'] : 0;
  607. if ($element['#step'] != 0 && webform_modulo($numeric_value - $starting_number, $element['#step']) != 0.0) {
  608. $samples = array(
  609. $starting_number,
  610. $starting_number + ($element['#step'] * 1),
  611. $starting_number + ($element['#step'] * 2),
  612. $starting_number + ($element['#step'] * 3),
  613. );
  614. if ($starting_number) {
  615. form_error($element, t('!name field value must be @start plus a multiple of @step. i.e. @samples, etc.', array('!name' => $element['#title'], '@start' => $element['#min'], '@step' => $element['#step'], '@samples' => implode(', ', $samples))));
  616. }
  617. else {
  618. form_error($element, t('!name field value must be a multiple of @step. i.e. @samples, etc.', array('!name' => $element['#title'], '@step' => $element['#step'], '@samples' => implode(', ', $samples))));
  619. }
  620. }
  621. }
  622. else {
  623. form_error($element, t('!name field value of @value must be numeric.', array('!name' => $element['#title'], '@value' => $value)));
  624. }
  625. }
  626. }
  627. /**
  628. * Implements _webform_submit_component().
  629. */
  630. function _webform_submit_number($component, $value) {
  631. // Because _webform_validate_number() ensures the format matches when moving
  632. // forward through a form, this should always pass before saving into the
  633. // database. When moving backwards in a form, do not adjust the value, since
  634. // it has not yet been validated.
  635. if (webform_number_format_match($value, $component['extra']['point'], $component['extra']['separator'])) {
  636. $value = webform_number_standardize($value, $component['extra']['point']);
  637. }
  638. return $value;
  639. }
  640. /**
  641. * Validation of number edit form items.
  642. */
  643. function _webform_edit_number_validate($element, &$form_state) {
  644. // Find the value of all related fields to this element.
  645. $parents = $element['#parents'];
  646. $key = array_pop($parents);
  647. $values = $form_state['values'];
  648. foreach ($parents as $parent) {
  649. $values = $values[$parent];
  650. }
  651. switch ($key) {
  652. case 'min':
  653. if ($values['min'] == '') {
  654. if (isset($values['type']) && $values['type'] === 'select') {
  655. form_error($element, t('Minimum is required when using a select list element.'));
  656. }
  657. }
  658. else {
  659. if (!is_numeric($values['min'])) {
  660. form_error($element, t('Minimum must be numeric.'));
  661. }
  662. if ($values['integer'] && filter_var((float) $values['min'], FILTER_VALIDATE_INT) === FALSE) {
  663. form_error($element, t('Minimum must have an integer value.'));
  664. }
  665. }
  666. break;
  667. case 'max':
  668. if ($values['max'] == '') {
  669. if (isset($values['type']) && $values['type'] === 'select') {
  670. form_error($element, t('Maximum is required when using a select list element.'));
  671. }
  672. }
  673. else {
  674. if (!is_numeric($values['max'])) {
  675. form_error($element, t('Maximum must be numeric.'));
  676. }
  677. if ($values['integer'] && filter_var((float) $values['max'], FILTER_VALIDATE_INT) === FALSE) {
  678. form_error($element, t('Maximum must have an integer value.'));
  679. }
  680. }
  681. break;
  682. case 'step':
  683. if ($values['step'] !== '') {
  684. if (!is_numeric($values['step'])) {
  685. form_error($element, t('Step must be numeric.'));
  686. }
  687. else {
  688. if ($values['integer'] && filter_var((float) $values['step'], FILTER_VALIDATE_INT) === FALSE) {
  689. form_error($element, t('Step must have an integer value.'));
  690. }
  691. }
  692. }
  693. break;
  694. }
  695. return TRUE;
  696. }
  697. /**
  698. * Generate select list options.
  699. */
  700. function _webform_number_select_options($component) {
  701. $options = array();
  702. $step = abs($component['extra']['step']);
  703. // Step is optional and defaults to 1.
  704. $step = empty($step) ? 1 : $step;
  705. // Generate list in correct direction.
  706. $min = $component['extra']['min'];
  707. $max = $component['extra']['max'];
  708. $flipped = FALSE;
  709. if ($max < $min) {
  710. $min = $component['extra']['max'];
  711. $max = $component['extra']['min'];
  712. $flipped = TRUE;
  713. }
  714. for ($f = $min; $f <= $max; $f += $step) {
  715. $options[$f . ''] = $f . '';
  716. }
  717. // @todo: HTML5 browsers apparently do not include the max value if it does
  718. // not line up with step. Restore this if needed in the future.
  719. // Add end limit if it's been skipped due to step.
  720. // @code
  721. // if (end($options) != $max) {
  722. // $options[$f] = $max;
  723. // }
  724. // @endcode
  725. if ($flipped) {
  726. $options = array_reverse($options, TRUE);
  727. }
  728. // Apply requisite number formatting.
  729. foreach ($options as $key => $value) {
  730. $options[$key] = _webform_number_format($component, $value);
  731. }
  732. return $options;
  733. }
  734. /**
  735. * Apply number format based on a component and number value.
  736. */
  737. function _webform_number_format($component, $value) {
  738. return webform_number_format($value, $component['extra']['decimals'], $component['extra']['point'], $component['extra']['separator']);
  739. }
  740. /**
  741. * Validates if a provided number string matches an expected format.
  742. *
  743. * This function allows the thousands separator to be optional, but decimal
  744. * points must be in the right location.
  745. *
  746. * A valid number is:
  747. * 1. optional minus sign.
  748. * 2. optional space.
  749. * 3. the rest of the string can't be just a decimal or blank.
  750. * 4. optional integer portion, with thousands separators.
  751. * 5. optional decimal portion, starting is a decimal separator.
  752. * Don't use preg_quote because a space is a valid thousands separator and
  753. * needs quoting for the 'x' option to preg_match.
  754. *
  755. * Based on http://stackoverflow.com/questions/5917082/regular-expression-to-match-numbers-with-or-without-commas-and-decimals-in-text.
  756. */
  757. function webform_number_format_match($value, $point, $separator) {
  758. $thousands = $separator ? "\\$separator?" : '';
  759. $decimal = "\\$point";
  760. return preg_match("/
  761. ^ # Start of string
  762. -? # Optional minus sign
  763. \ ? # Optional space
  764. (?!\.?$) # Assert looking ahead, not just a decimal or nothing
  765. (?: # Interger portion (non-grouping)
  766. \d{1,3} # 1 to 3 digits
  767. (?: # Thousands group(s)
  768. $thousands # Optional thousands separator
  769. \d{2,3} # 2 or 3 digits. Some countries use groups of 2 sometimes
  770. )* # 0 or more of these thousands groups
  771. )? # End of optional integer portion
  772. (?: # Decimal portion (non-grouping)
  773. $decimal # Decimal point
  774. \d* # 0 or more digits
  775. )? # End of optional decimal portion
  776. $
  777. /x", $value);
  778. }
  779. /**
  780. * Format a number with thousands separator, decimal point, and decimal places.
  781. *
  782. * This function is a wrapper around PHP's native number_format(), but allows
  783. * the decimal places parameter to be NULL or an empty string, resulting in a
  784. * behavior of no change to the decimal places.
  785. */
  786. function webform_number_format($value, $decimals = NULL, $point = '.', $separator = ',') {
  787. if (!is_numeric($value)) {
  788. return '';
  789. }
  790. // If no decimal places are specified, do a best guess length of decimals.
  791. if (is_null($decimals) || $decimals === '') {
  792. // If it's an integer, no decimals needed.
  793. if (filter_var((float) $value, FILTER_VALIDATE_INT) !== FALSE) {
  794. $decimals = 0;
  795. }
  796. else {
  797. $decimals = strlen($value) - strrpos($value, '.') - 1;
  798. }
  799. if ($decimals > 4) {
  800. $decimals = 4;
  801. }
  802. }
  803. return number_format($value, $decimals, $point, $separator);
  804. }
  805. /**
  806. * Given a number, convert it to string compatible with a PHP float.
  807. *
  808. * @param string $value
  809. * The string value to be standardized into a numeric string.
  810. * @param string $point
  811. * The point separator between the whole number and the decimals.
  812. *
  813. * @return string
  814. * The converted number.
  815. */
  816. function webform_number_standardize($value, $point) {
  817. // For simplicity, strip everything that's not the decimal point.
  818. $value = preg_replace('/[^\-0-9' . preg_quote($point, '/') . ']/', '', $value);
  819. // Convert the decimal point to a period.
  820. $value = str_replace($point, '.', $value);
  821. return $value;
  822. }
  823. /**
  824. * Custom modulo function that properly handles float division.
  825. *
  826. * See https://drupal.org/node/1601968.
  827. */
  828. function webform_modulo($a, $b) {
  829. $modulo = $a - $b * (($b < 0) ? ceil($a / $b) : floor($a / $b));
  830. if (webform_compare_floats($modulo, 0.0) == 0 || webform_compare_floats($modulo, $b) == 0) {
  831. $modulo = 0.0;
  832. }
  833. return $modulo;
  834. }
  835. /**
  836. * Compare two floats.
  837. *
  838. * See @link http://php.net/manual/en/language.types.float.php @endlink.
  839. *
  840. * Comparison of floating point numbers for equality is surprisingly difficult,
  841. * as evidenced by the references below. The simple test in this function works
  842. * for numbers that are relatively close to 1E1. For very small numbers, it will
  843. * show false equality. For very large numbers, it will show false inequality.
  844. * Better implementations are hidered by the absense of PHP platform-specific
  845. * floating point constants to properly set the minimum absolute and relative
  846. * error in PHP.
  847. *
  848. * The use case for webform conditionals excludes very small or very large
  849. * numeric comparisons.
  850. *
  851. * See @link http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm @endlink
  852. * See @link http://floating-point-gui.de/errors/comparison/ @endlink
  853. * See @link http://en.wikipedia.org/wiki/IEEE_754-1985#Denormalized_numbers @endlink
  854. *
  855. * @param float $number_1
  856. * The first number.
  857. * @param float $number_2
  858. * The second number.
  859. *
  860. * @return int|null
  861. * < 0 if number_1 is less than number_2; > 0 if number_1 is greater than
  862. * number_2, 0 if they are equal, and NULL if either is not numeric.
  863. */
  864. function webform_compare_floats($number_1, $number_2) {
  865. if (!is_numeric($number_1) || !is_numeric($number_2)) {
  866. return NULL;
  867. }
  868. $number_1 = (float) $number_1;
  869. $number_2 = (float) $number_2;
  870. $epsilon = 0.000001;
  871. if (abs($number_1 - $number_2) < $epsilon) {
  872. return 0;
  873. }
  874. elseif ($number_1 > $number_2) {
  875. return 1;
  876. }
  877. else {
  878. return -1;
  879. }
  880. }