xref: /plugin/struct/types/Decimal.php (revision 7234bfb14e712ff548d9266ef32fdcc8eaf2d04e)
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