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 = [ 19 'min' => '', 20 'max' => '', 21 'roundto' => '-1', 22 'decpoint' => '.', 23 'thousands' => "\xE2\x80\xAF", 24 // narrow no-break space 25 'trimzeros' => true, 26 'prefix' => '', 27 'postfix' => '', 28 'engineering' => false, 29 ]; 30 31 /** 32 * Output the stored data 33 * 34 * @param string|int $value the value stored in the database 35 * @param \Doku_Renderer $R the renderer currently used to render the data 36 * @param string $mode The mode the output is rendered in (eg. XHTML) 37 * @return bool true if $mode could be satisfied 38 */ 39 public function renderValue($value, \Doku_Renderer $R, $mode) 40 { 41 42 if ($this->config['engineering']) { 43 $unitsh = ['', 'k', 'M', 'G', 'T']; 44 $unitsl = ['', 'm', 'µ', 'n', 'p', 'f', 'a']; 45 46 $exp = floor(log10($value) / 3); 47 48 if ($exp < 0) { 49 $units = $unitsl; 50 $pfkey = -1 * $exp; 51 } else { 52 $units = $unitsh; 53 $pfkey = $exp; 54 } 55 56 if (count($units) <= ($pfkey + 1)) { //check if number is within prefixes 57 $pfkey = count($units) - 1; 58 $exp = $pfkey * $exp / abs($exp); 59 } 60 61 $R->cdata( 62 $this->config['prefix'] . 63 $value / 10 ** ($exp * 3) . "\xE2\x80\xAF" . $units[$pfkey] . 64 $this->config['postfix'] 65 ); 66 return true; 67 } 68 69 70 if ($this->config['roundto'] == -1) { 71 $value = $this->formatWithoutRounding( 72 $value, 73 $this->config['decpoint'], 74 $this->config['thousands'] 75 ); 76 } else { 77 $value = (float) $value; 78 $value = number_format( 79 $value, 80 (int)$this->config['roundto'], 81 $this->config['decpoint'], 82 $this->config['thousands'] 83 ); 84 } 85 if ($this->config['trimzeros'] && (strpos($value, $this->config['decpoint']) !== false)) { 86 $value = rtrim($value, '0'); 87 $value = rtrim($value, $this->config['decpoint']); 88 } 89 90 91 $R->cdata($this->config['prefix'] . $value . $this->config['postfix']); 92 return true; 93 } 94 95 /** 96 * @param int|string $rawvalue 97 * @return int|string 98 * @throws ValidationException 99 */ 100 public function validate($rawvalue) 101 { 102 $rawvalue = parent::validate($rawvalue); 103 $rawvalue = str_replace(',', '.', $rawvalue); // we accept both 104 105 if ((string)$rawvalue !== (string)(float) $rawvalue) { 106 throw new ValidationException('Decimal needed'); 107 } 108 109 if ($this->config['min'] !== '' && (float) $rawvalue < (float) $this->config['min']) { 110 throw new ValidationException('Decimal min', (float) $this->config['min']); 111 } 112 113 if ($this->config['max'] !== '' && (float) $rawvalue > (float) $this->config['max']) { 114 throw new ValidationException('Decimal max', (float) $this->config['max']); 115 } 116 117 return $rawvalue; 118 } 119 120 /** 121 * Works like number_format but keeps the decimals as is 122 * 123 * @link http://php.net/manual/en/function.number-format.php#91047 124 * @author info at daniel-marschall dot de 125 * @param float $number 126 * @param string $dec_point 127 * @param string $thousands_sep 128 * @return string 129 */ 130 protected function formatWithoutRounding($number, $dec_point, $thousands_sep) 131 { 132 $was_neg = $number < 0; // Because +0 == -0 133 134 $tmp = explode('.', $number); 135 $out = number_format(abs((float) $tmp[0]), 0, $dec_point, $thousands_sep); 136 if (isset($tmp[1])) $out .= $dec_point . $tmp[1]; 137 138 if ($was_neg) $out = "-$out"; 139 140 return $out; 141 } 142 143 /** 144 * Decimals need to be casted to the proper type for sorting 145 * 146 * @param QueryBuilder $QB 147 * @param string $tablealias 148 * @param string $colname 149 * @param string $order 150 */ 151 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 152 { 153 $QB->addOrderBy("CAST($tablealias.$colname AS DECIMAL) $order"); 154 } 155 156 /** 157 * Decimals need to be casted to proper type for comparison 158 * 159 * @param QueryBuilderWhere $add 160 * @param string $tablealias 161 * @param string $colname 162 * @param string $comp 163 * @param string|\string[] $value 164 * @param string $op 165 */ 166 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 167 { 168 $add = $add->where($op); // open a subgroup 169 $add->where('AND', "$tablealias.$colname != ''"); 170 // make sure the field isn't empty 171 $op = 'AND'; 172 173 /** @var QueryBuilderWhere $add Where additionional queries are added to */ 174 if (is_array($value)) { 175 $add = $add->where($op); // sub where group 176 $op = 'OR'; 177 } 178 179 foreach ((array)$value as $item) { 180 $pl = $add->getQB()->addValue($item); 181 $add->where($op, "CAST($tablealias.$colname AS DECIMAL) $comp CAST($pl AS DECIMAL)"); 182 } 183 } 184 185 /** 186 * Only exact matches for numbers 187 * 188 * @return string 189 */ 190 public function getDefaultComparator() 191 { 192 return '='; 193 } 194} 195