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