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        '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 = array('', 'k', 'M', 'G', 'T');
43            $unitsl = array('', '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 = sizeof($units)-1;
57                $exp   = $pfkey * $exp/abs($exp);
58            }
59
60            $R->cdata($this->config['prefix'] . $value / 10**($exp*3) . "\xE2\x80\xAF" . $units[$pfkey] . $this->config['postfix'] );
61            return true;
62        }
63
64
65        if ($this->config['roundto'] == -1) {
66            $value = $this->formatWithoutRounding(
67                $value,
68                $this->config['decpoint'],
69                $this->config['thousands']
70            );
71        } else {
72            $value = floatval($value);
73            $value = number_format(
74                $value,
75                $this->config['roundto'],
76                $this->config['decpoint'],
77                $this->config['thousands']
78            );
79        }
80        if ($this->config['trimzeros'] && (strpos($value, $this->config['decpoint']) !== false)) {
81            $value = rtrim($value, '0');
82            $value = rtrim($value, $this->config['decpoint']);
83        }
84
85
86        $R->cdata($this->config['prefix'] . $value . $this->config['postfix'] );
87        return true;
88    }
89
90    /**
91     * @param int|string $rawvalue
92     * @return int|string
93     * @throws ValidationException
94     */
95    public function validate($rawvalue)
96    {
97        $rawvalue = parent::validate($rawvalue);
98        $rawvalue = str_replace(',', '.', $rawvalue); // we accept both
99
100        if ((string)$rawvalue != (string)floatval($rawvalue)) {
101            throw new ValidationException('Decimal needed');
102        }
103
104        if ($this->config['min'] !== '' && floatval($rawvalue) < floatval($this->config['min'])) {
105            throw new ValidationException('Decimal min', floatval($this->config['min']));
106        }
107
108        if ($this->config['max'] !== '' && floatval($rawvalue) > floatval($this->config['max'])) {
109            throw new ValidationException('Decimal max', floatval($this->config['max']));
110        }
111
112        return $rawvalue;
113    }
114
115    /**
116     * Works like number_format but keeps the decimals as is
117     *
118     * @link http://php.net/manual/en/function.number-format.php#91047
119     * @author info at daniel-marschall dot de
120     * @param float $number
121     * @param string $dec_point
122     * @param string $thousands_sep
123     * @return string
124     */
125    protected function formatWithoutRounding($number, $dec_point, $thousands_sep)
126    {
127        $was_neg = $number < 0; // Because +0 == -0
128
129        $tmp = explode('.', $number);
130        $out = number_format(abs(floatval($tmp[0])), 0, $dec_point, $thousands_sep);
131        if (isset($tmp[1])) $out .= $dec_point . $tmp[1];
132
133        if ($was_neg) $out = "-$out";
134
135        return $out;
136    }
137
138    /**
139     * Decimals need to be casted to the proper type for sorting
140     *
141     * @param QueryBuilder $QB
142     * @param string $tablealias
143     * @param string $colname
144     * @param string $order
145     */
146    public function sort(QueryBuilder $QB, $tablealias, $colname, $order)
147    {
148        $QB->addOrderBy("CAST($tablealias.$colname AS DECIMAL) $order");
149    }
150
151    /**
152     * Decimals need to be casted to proper type for comparison
153     *
154     * @param QueryBuilderWhere $add
155     * @param string $tablealias
156     * @param string $colname
157     * @param string $comp
158     * @param string|\string[] $value
159     * @param string $op
160     */
161    public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op)
162    {
163        $add = $add->where($op); // open a subgroup
164        $add->where('AND', "$tablealias.$colname != ''"); // make sure the field isn't empty
165        $op = 'AND';
166
167        /** @var QueryBuilderWhere $add Where additionional queries are added to */
168        if (is_array($value)) {
169            $add = $add->where($op); // sub where group
170            $op = 'OR';
171        }
172
173        foreach ((array)$value as $item) {
174            $pl = $add->getQB()->addValue($item);
175            $add->where($op, "CAST($tablealias.$colname AS DECIMAL) $comp CAST($pl AS DECIMAL)");
176        }
177    }
178
179    /**
180     * Only exact matches for numbers
181     *
182     * @return string
183     */
184    public function getDefaultComparator()
185    {
186        return '=';
187    }
188}
189