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