From 6c29350918e0c5b8cadf8b0479a6ef8c8cf7ec0c Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Tue, 11 Jan 2022 09:51:52 -0500 Subject: [PATCH] Additional updates, additions and fixes to $sanitizer->float(), InputfieldFloat and FieldtypeFloat per processwire/processwire-issues#1502 --- wire/core/Sanitizer.php | 35 ++++++++++++---- wire/modules/Fieldtype/FieldtypeFloat.module | 26 ++++++++---- .../modules/Inputfield/InputfieldFloat.module | 41 +++++++++++++------ 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/wire/core/Sanitizer.php b/wire/core/Sanitizer.php index 316fce4f..8858920f 100644 --- a/wire/core/Sanitizer.php +++ b/wire/core/Sanitizer.php @@ -3880,6 +3880,15 @@ class Sanitizer extends Wire { /** * Sanitize to floating point value * + * Values for `getString` argument: + * + * - `false` (bool): do not return string value (default). 3.0.171+ + * - `true` (bool): locale aware floating point number string. 3.0.171+ + * - `f` (string): locale aware floating point number string (same as true). 3.0.193+ + * - `F` (string): non-locale aware floating point number string. 3.0.193+ + * - `e` (string): lowercase scientific notation (e.g. 1.2e+2). 3.0.193+ + * - `E` (string): uppercase scientific notation (e.g. 1.2E+2). 3.0.193+ + * * #pw-group-numbers * * @param float|string|int $value @@ -3889,8 +3898,8 @@ class Sanitizer extends Wire { * - `blankValue` (null|int|string|float): Value to return (whether float or non-float) if provided $value is an empty non-float (default=0.0) * - `min` (float|null): Minimum allowed value, excluding blankValue (default=null) * - `max` (float|null): Maximum allowed value, excluding blankValue (default=null) - * - `getString (bool): Return a string rather than float value? (default=false) added 3.0.171 - * @return float + * - `getString (bool|string): Return a string rather than float value? 3.0.171+ (default=false). See value options in method description. + * @return float|string * */ public function float($value, array $options = array()) { @@ -3901,7 +3910,7 @@ class Sanitizer extends Wire { 'blankValue' => 0.0, // Value to return (whether float or non-float) if provided $value is an empty non-float (default=0.0) 'min' => null, // Minimum allowed value (excluding blankValue) 'max' => null, // Maximum allowed value (excluding blankValue) - 'getString' => false, // Return a string rather than float value? + 'getString' => false, // Return a string rather than float value? bool or f, F, e, E ); $options = array_merge($defaults, $options); @@ -3926,10 +3935,11 @@ class Sanitizer extends Wire { $prepend = '-'; $str = ltrim($str, '-'); } - - if((stripos($str, 'E-') || stripos($str, 'E+')) && preg_match('/^([0-9., ]+\d)(E[-+]\d+)/i', $str, $m)) { + + if(stripos($str, 'E') && preg_match('/^([0-9., ]*\d)(E[-+]?\d+)/i', $str, $m)) { $str = $m[1]; $append = $m[2]; + if($options['precision'] === null) $options['precision'] = ((int) ltrim($append, '-+eE')); } if(!strlen($str)) return $options['blankValue']; @@ -3975,7 +3985,13 @@ class Sanitizer extends Wire { $value = $prepend . $value . $append; if(!$options['getString']) $value = floatval($value); - } + + } else if(is_float($value)) { + if($options['precision'] === null) { + $str = strtoupper("$value"); + if(strpos($str, 'E')) $options['precision'] = (int) ltrim(stristr("$value", 'E'), 'E-+'); + } + } if(!$options['getString'] && !is_float($value)) $value = (float) $value; if(!is_null($options['min']) && ((float) $value) < ((float) $options['min'])) $value = $options['min']; @@ -3983,11 +3999,14 @@ class Sanitizer extends Wire { if(!is_null($options['precision'])) $value = round((float) $value, (int) $options['precision'], (int) $options['mode']); if($options['getString']) { + $f = $options['getString']; + $f = is_string($f) && in_array($f, array('f', 'F', 'e', 'E')) ? $f : 'f'; if($options['precision'] === null) { - $value = strpos($value, 'E-') || strpos($value, 'E+') ? rtrim(sprintf('%.20f', (float) $value), '0') : "$value"; + $value = stripos("$value", 'E') ? rtrim(sprintf("%.15$f", (float) $value), '0') : "$value"; } else { - $value = sprintf('%.' . $options['precision'] . 'f', (float) $value); + $value = sprintf("%.$options[precision]$f", (float) $value); } + $value = rtrim($value, '.'); } return $value; diff --git a/wire/modules/Fieldtype/FieldtypeFloat.module b/wire/modules/Fieldtype/FieldtypeFloat.module index a1bb210e..4b9b4d1e 100644 --- a/wire/modules/Fieldtype/FieldtypeFloat.module +++ b/wire/modules/Fieldtype/FieldtypeFloat.module @@ -8,7 +8,7 @@ * For documentation about the fields used in this class, please see: * /wire/core/Fieldtype.php * - * ProcessWire 3.x, Copyright 2020 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * */ @@ -19,7 +19,7 @@ class FieldtypeFloat extends Fieldtype { return array( 'title' => __('Float', __FILE__), 'summary' => __('Field that stores a floating point number', __FILE__), - 'version' => 106, + 'version' => 107, 'permanent' => true, ); } @@ -86,7 +86,7 @@ class FieldtypeFloat extends Fieldtype { $value = $this->wire()->sanitizer->float((string) $value, array('blankValue' => '')); } $precision = $field->get('precision'); - if($precision === null || $precision === '') { + if($precision === null || $precision === '' || $precision < 0) { $value = (float) $value; } else { $value = round((float) $value, $precision); @@ -121,9 +121,13 @@ class FieldtypeFloat extends Fieldtype { */ public function ___sleepValue(Page $page, Field $field, $value) { $precision = $field->get('precision'); - if(is_null($precision) || $precision === '') $precision = self::getPrecision($value); - if(!is_string($value)) $value = number_format($value, $precision, '.', ''); - return $value; + if(is_null($precision) || $precision === '' || $precision < 0) { + $precision = self::getPrecision($value); + } + if(!is_string($value)) { + $value = number_format($value, $precision, '.', ''); + } + return $value; } /** @@ -135,8 +139,13 @@ class FieldtypeFloat extends Fieldtype { */ public static function getPrecision($value) { $value = (float) $value; - $remainder = ceil($value) - $value; - $precision = strlen(ltrim($remainder, '0., ')); + if(stripos("$value", 'E')) { + list(,$precision) = explode('E', strtoupper("$value"), 2); + $precision = (int) ltrim($precision, '+-'); + } else { + $remainder = ceil($value) - $value; + $precision = strlen(ltrim("$remainder", '0., ')); + } if(!$precision) $precision = 1; return $precision; } @@ -170,6 +179,7 @@ class FieldtypeFloat extends Fieldtype { $f = $this->wire()->modules->get('InputfieldInteger'); $f->attr('name', 'precision'); $f->label = $this->_('Number of decimal digits to round to'); + $f->description = $this->_('Or use a negative number like `-1` to disable rounding.'); if($precision !== '') $f->val($precision); $f->attr('size', 8); $inputfields->append($f); diff --git a/wire/modules/Inputfield/InputfieldFloat.module b/wire/modules/Inputfield/InputfieldFloat.module index 99c9f737..62364b23 100644 --- a/wire/modules/Inputfield/InputfieldFloat.module +++ b/wire/modules/Inputfield/InputfieldFloat.module @@ -6,7 +6,7 @@ * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * - * @property int $precision Decimals precision + * @property int $precision Decimals precision (or -1 to disable rounding in 3.0.193+) * @property int $digits Total digits, for when used in decimal mode (default=0) * @property string $inputType Input type to use, one of "text" or "number" * @property int|float $min @@ -25,7 +25,7 @@ class InputfieldFloat extends InputfieldInteger { return array( 'title' => __('Float', __FILE__), // Module Title 'summary' => __('Floating point number with precision', __FILE__), // Module Summary - 'version' => 104, + 'version' => 105, 'permanent' => true, ); } @@ -59,7 +59,7 @@ class InputfieldFloat extends InputfieldInteger { protected function getPrecision($value = null) { if($value !== null) return FieldtypeFloat::getPrecision($value); $precision = $this->precision; - return $precision === null || $precision === '' ? '' : (int) $precision; + return $precision === null || $precision === '' || $precision < 0 ? '' : (int) $precision; } /** @@ -78,10 +78,8 @@ class InputfieldFloat extends InputfieldInteger { if(!strlen("$value")) return ''; } $precision = $this->precision; - if($precision === null || $precision === '') { - $precision = FieldtypeFloat::getPrecision($value); - } - return round((float) $value, $precision); + if($precision === null || $precision === '') $precision = $this->getPrecision($value); + return is_int($precision) && $precision > 0 ? round((float) $value, $precision) : $value; } /** @@ -95,6 +93,23 @@ class InputfieldFloat extends InputfieldInteger { return (float) $value; } + /** + * Is value in scientific notation? + * + * @param string $value + * @return bool + * @since 3.0.193 + * + */ + public function isScientific($value) { + $value = strtoupper((string) $value); + if(strpos($value, 'E') === false) return false; + $value = str_replace('.', '', $value); + list($a, $b) = explode('E', $value, 2); + $b = trim($b, '+-'); + return ctype_digit("$a$b"); + } + /** * Override method from Inputfield to convert locale specific decimals for input[type=number] * @@ -105,8 +120,8 @@ class InputfieldFloat extends InputfieldInteger { public function getAttributesString(array $attributes = null) { if($attributes && $attributes['type'] === 'number') { $value = isset($attributes['value']) ? $attributes['value'] : null; - if(is_float($value) || is_string($value)) { - if(strlen("$value") && !ctype_digit(str_replace('.', '', ltrim($value, '-')))) { + if(is_float($value) || (is_string($value) && strlen($value))) { + if(!$this->isScientific($value) && !ctype_digit(str_replace('.', '', ltrim($value, '-')))) { // float value is using a non "." as decimal point, needs conversion because // the HTML5 number input type requires "." as the decimal $attributes['value'] = $this->localeConvertValue($value); @@ -117,10 +132,8 @@ class InputfieldFloat extends InputfieldInteger { $attributes['step'] = '.' . ($precision > 1 ? str_repeat('0', $precision - 1) : '') . '1'; } } - if($attributes && isset($attributes['value']) && stripos($attributes['value'], 'E')) { - $attributes['value'] = $this->wire()->sanitizer->float($attributes['value'], array( - 'getString' => true, - )); + if($attributes && isset($attributes['value']) && $this->isScientific($attributes['value'])) { + $attributes['value'] = $this->wire()->sanitizer->float($attributes['value']); } return parent::getAttributesString($attributes); } @@ -152,10 +165,12 @@ class InputfieldFloat extends InputfieldInteger { public function getConfigInputfields() { $inputfields = parent::getConfigInputfields(); if($this->hasFieldtype === false) { + // when used without FieldtypeFloat /** @var InputfieldInteger $f */ $f = $this->wire()->modules->get('InputfieldInteger'); $f->attr('name', 'precision'); $f->label = $this->_('Number of decimal digits to round to'); + $f->description = $this->_('Or use a negative number like `-1` to disable rounding.'); $f->attr('value', $this->precision); $f->attr('size', 8); $inputfields->add($f);