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