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