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