1 <?php
2 
3 namespace dokuwiki\plugin\struct\types;
4 
5 use dokuwiki\plugin\struct\meta\QueryBuilder;
6 use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
7 use 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  */
16 class 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