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