1<?php 2 3namespace dokuwiki\plugin\struct\types; 4 5use dokuwiki\plugin\struct\meta\QueryBuilder; 6use dokuwiki\plugin\struct\meta\QueryBuilderWhere; 7use dokuwiki\plugin\struct\meta\ValidationException; 8 9/** 10 * Class Decimal 11 * 12 * A field accepting decimal numbers 13 * 14 * @package dokuwiki\plugin\struct\types 15 */ 16class Decimal extends AbstractMultiBaseType 17{ 18 protected $config = array( 19 'min' => '', 20 'max' => '', 21 'roundto' => '-1', 22 'decpoint' => '.', 23 'thousands' => "\xE2\x80\xAF", // narrow no-break space 24 'trimzeros' => true, 25 'prefix' => '', 26 'postfix' => '' 27 ); 28 29 /** 30 * Output the stored data 31 * 32 * @param string|int $value the value stored in the database 33 * @param \Doku_Renderer $R the renderer currently used to render the data 34 * @param string $mode The mode the output is rendered in (eg. XHTML) 35 * @return bool true if $mode could be satisfied 36 */ 37 public function renderValue($value, \Doku_Renderer $R, $mode) 38 { 39 if ($this->config['roundto'] == -1) { 40 $value = $this->formatWithoutRounding( 41 $value, 42 $this->config['decpoint'], 43 $this->config['thousands'] 44 ); 45 } else { 46 $value = floatval($value); 47 $value = number_format( 48 $value, 49 $this->config['roundto'], 50 $this->config['decpoint'], 51 $this->config['thousands'] 52 ); 53 } 54 if ($this->config['trimzeros'] && (strpos($value, $this->config['decpoint']) !== false)) { 55 $value = rtrim($value, '0'); 56 $value = rtrim($value, $this->config['decpoint']); 57 } 58 59 $R->cdata($this->config['prefix'] . $value . $this->config['postfix']); 60 return true; 61 } 62 63 /** 64 * @param int|string $rawvalue 65 * @return int|string 66 * @throws ValidationException 67 */ 68 public function validate($rawvalue) 69 { 70 $rawvalue = parent::validate($rawvalue); 71 $rawvalue = str_replace(',', '.', $rawvalue); // we accept both 72 73 if ((string)$rawvalue != (string)floatval($rawvalue)) { 74 throw new ValidationException('Decimal needed'); 75 } 76 77 if ($this->config['min'] !== '' && floatval($rawvalue) < floatval($this->config['min'])) { 78 throw new ValidationException('Decimal min', floatval($this->config['min'])); 79 } 80 81 if ($this->config['max'] !== '' && floatval($rawvalue) > floatval($this->config['max'])) { 82 throw new ValidationException('Decimal max', floatval($this->config['max'])); 83 } 84 85 return $rawvalue; 86 } 87 88 /** 89 * Works like number_format but keeps the decimals as is 90 * 91 * @link http://php.net/manual/en/function.number-format.php#91047 92 * @author info at daniel-marschall dot de 93 * @param float $number 94 * @param string $dec_point 95 * @param string $thousands_sep 96 * @return string 97 */ 98 protected function formatWithoutRounding($number, $dec_point, $thousands_sep) 99 { 100 $was_neg = $number < 0; // Because +0 == -0 101 102 $tmp = explode('.', $number); 103 $out = number_format(abs(floatval($tmp[0])), 0, $dec_point, $thousands_sep); 104 if (isset($tmp[1])) $out .= $dec_point . $tmp[1]; 105 106 if ($was_neg) $out = "-$out"; 107 108 return $out; 109 } 110 111 /** 112 * Decimals need to be casted to the proper type for sorting 113 * 114 * @param QueryBuilder $QB 115 * @param string $tablealias 116 * @param string $colname 117 * @param string $order 118 */ 119 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 120 { 121 $QB->addOrderBy("CAST($tablealias.$colname AS DECIMAL) $order"); 122 } 123 124 /** 125 * Decimals need to be casted to proper type for comparison 126 * 127 * @param QueryBuilderWhere $add 128 * @param string $tablealias 129 * @param string $colname 130 * @param string $comp 131 * @param string|\string[] $value 132 * @param string $op 133 */ 134 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 135 { 136 $add = $add->where($op); // open a subgroup 137 $add->where('AND', "$tablealias.$colname != ''"); // make sure the field isn't empty 138 $op = 'AND'; 139 140 /** @var QueryBuilderWhere $add Where additionional queries are added to */ 141 if (is_array($value)) { 142 $add = $add->where($op); // sub where group 143 $op = 'OR'; 144 } 145 146 foreach ((array)$value as $item) { 147 $pl = $add->getQB()->addValue($item); 148 $add->where($op, "CAST($tablealias.$colname AS DECIMAL) $comp CAST($pl AS DECIMAL)"); 149 } 150 } 151 152 /** 153 * Only exact matches for numbers 154 * 155 * @return string 156 */ 157 public function getDefaultComparator() 158 { 159 return '='; 160 } 161} 162