| 1441 |
ariadna |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
|
|
|
4 |
|
|
|
5 |
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
|
|
6 |
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
|
|
7 |
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
|
|
|
8 |
|
|
|
9 |
abstract class DatabaseAbstract
|
|
|
10 |
{
|
|
|
11 |
abstract public static function evaluate(array $database, array|null|int|string $field, array $criteria): null|float|int|string;
|
|
|
12 |
|
|
|
13 |
/**
|
|
|
14 |
* fieldExtract.
|
|
|
15 |
*
|
|
|
16 |
* Extracts the column ID to use for the data field.
|
|
|
17 |
*
|
|
|
18 |
* @param mixed[] $database The range of cells that makes up the list or database.
|
|
|
19 |
* A database is a list of related data in which rows of related
|
|
|
20 |
* information are records, and columns of data are fields. The
|
|
|
21 |
* first row of the list contains labels for each column.
|
|
|
22 |
* @param mixed $field Indicates which column is used in the function. Enter the
|
|
|
23 |
* column label enclosed between double quotation marks, such as
|
|
|
24 |
* "Age" or "Yield," or a number (without quotation marks) that
|
|
|
25 |
* represents the position of the column within the list: 1 for
|
|
|
26 |
* the first column, 2 for the second column, and so on.
|
|
|
27 |
*/
|
|
|
28 |
protected static function fieldExtract(array $database, mixed $field): ?int
|
|
|
29 |
{
|
|
|
30 |
$field = strtoupper(Functions::flattenSingleValue($field) ?? '');
|
|
|
31 |
if ($field === '') {
|
|
|
32 |
return null;
|
|
|
33 |
}
|
|
|
34 |
|
|
|
35 |
$fieldNames = array_map('strtoupper', array_shift($database));
|
|
|
36 |
if (is_numeric($field)) {
|
|
|
37 |
$field = (int) $field - 1;
|
|
|
38 |
if ($field < 0 || $field >= count($fieldNames)) {
|
|
|
39 |
return null;
|
|
|
40 |
}
|
|
|
41 |
|
|
|
42 |
return $field;
|
|
|
43 |
}
|
|
|
44 |
$key = array_search($field, array_values($fieldNames), true);
|
|
|
45 |
|
|
|
46 |
return ($key !== false) ? (int) $key : null;
|
|
|
47 |
}
|
|
|
48 |
|
|
|
49 |
/**
|
|
|
50 |
* filter.
|
|
|
51 |
*
|
|
|
52 |
* Parses the selection criteria, extracts the database rows that match those criteria, and
|
|
|
53 |
* returns that subset of rows.
|
|
|
54 |
*
|
|
|
55 |
* @param mixed[] $database The range of cells that makes up the list or database.
|
|
|
56 |
* A database is a list of related data in which rows of related
|
|
|
57 |
* information are records, and columns of data are fields. The
|
|
|
58 |
* first row of the list contains labels for each column.
|
|
|
59 |
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
|
|
|
60 |
* You can use any range for the criteria argument, as long as it
|
|
|
61 |
* includes at least one column label and at least one cell below
|
|
|
62 |
* the column label in which you specify a condition for the
|
|
|
63 |
* column.
|
|
|
64 |
*
|
|
|
65 |
* @return mixed[]
|
|
|
66 |
*/
|
|
|
67 |
protected static function filter(array $database, array $criteria): array
|
|
|
68 |
{
|
|
|
69 |
$fieldNames = array_shift($database);
|
|
|
70 |
$criteriaNames = array_shift($criteria);
|
|
|
71 |
|
|
|
72 |
// Convert the criteria into a set of AND/OR conditions with [:placeholders]
|
|
|
73 |
$query = self::buildQuery($criteriaNames, $criteria);
|
|
|
74 |
|
|
|
75 |
// Loop through each row of the database
|
|
|
76 |
return self::executeQuery($database, $query, $criteriaNames, $fieldNames);
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array
|
|
|
80 |
{
|
|
|
81 |
// reduce the database to a set of rows that match all the criteria
|
|
|
82 |
$database = self::filter($database, $criteria);
|
|
|
83 |
$defaultReturnColumnValue = ($field === null) ? 1 : null;
|
|
|
84 |
|
|
|
85 |
// extract an array of values for the requested column
|
|
|
86 |
$columnData = [];
|
|
|
87 |
foreach ($database as $rowKey => $row) {
|
|
|
88 |
$keys = array_keys($row);
|
|
|
89 |
$key = $keys[$field] ?? null;
|
|
|
90 |
$columnKey = $key ?? 'A';
|
|
|
91 |
$columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue;
|
|
|
92 |
}
|
|
|
93 |
|
|
|
94 |
return $columnData;
|
|
|
95 |
}
|
|
|
96 |
|
|
|
97 |
private static function buildQuery(array $criteriaNames, array $criteria): string
|
|
|
98 |
{
|
|
|
99 |
$baseQuery = [];
|
|
|
100 |
foreach ($criteria as $key => $criterion) {
|
|
|
101 |
foreach ($criterion as $field => $value) {
|
|
|
102 |
$criterionName = $criteriaNames[$field];
|
|
|
103 |
if ($value !== null) {
|
|
|
104 |
$condition = self::buildCondition($value, $criterionName);
|
|
|
105 |
$baseQuery[$key][] = $condition;
|
|
|
106 |
}
|
|
|
107 |
}
|
|
|
108 |
}
|
|
|
109 |
|
|
|
110 |
$rowQuery = array_map(
|
|
|
111 |
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore-line
|
|
|
112 |
$baseQuery
|
|
|
113 |
);
|
|
|
114 |
|
|
|
115 |
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : ($rowQuery[0] ?? '');
|
|
|
116 |
}
|
|
|
117 |
|
|
|
118 |
private static function buildCondition(mixed $criterion, string $criterionName): string
|
|
|
119 |
{
|
|
|
120 |
$ifCondition = Functions::ifCondition($criterion);
|
|
|
121 |
|
|
|
122 |
// Check for wildcard characters used in the condition
|
|
|
123 |
$result = preg_match('/(?<operator>[^"]*)(?<operand>".*[*?].*")/ui', $ifCondition, $matches);
|
|
|
124 |
if ($result !== 1) {
|
|
|
125 |
return "[:{$criterionName}]{$ifCondition}";
|
|
|
126 |
}
|
|
|
127 |
|
|
|
128 |
$trueFalse = ($matches['operator'] !== '<>');
|
|
|
129 |
$wildcard = WildcardMatch::wildcard($matches['operand']);
|
|
|
130 |
$condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})";
|
|
|
131 |
if ($trueFalse === false) {
|
|
|
132 |
$condition = "NOT({$condition})";
|
|
|
133 |
}
|
|
|
134 |
|
|
|
135 |
return $condition;
|
|
|
136 |
}
|
|
|
137 |
|
|
|
138 |
private static function executeQuery(array $database, string $query, array $criteria, array $fields): array
|
|
|
139 |
{
|
|
|
140 |
foreach ($database as $dataRow => $dataValues) {
|
|
|
141 |
// Substitute actual values from the database row for our [:placeholders]
|
|
|
142 |
$conditions = $query;
|
|
|
143 |
foreach ($criteria as $criterion) {
|
|
|
144 |
$conditions = self::processCondition($criterion, $fields, $dataValues, $conditions);
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
// evaluate the criteria against the row data
|
|
|
148 |
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions);
|
|
|
149 |
|
|
|
150 |
// If the row failed to meet the criteria, remove it from the database
|
|
|
151 |
if ($result !== true) {
|
|
|
152 |
unset($database[$dataRow]);
|
|
|
153 |
}
|
|
|
154 |
}
|
|
|
155 |
|
|
|
156 |
return $database;
|
|
|
157 |
}
|
|
|
158 |
|
|
|
159 |
private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions): string
|
|
|
160 |
{
|
|
|
161 |
$key = array_search($criterion, $fields, true);
|
|
|
162 |
|
|
|
163 |
$dataValue = 'NULL';
|
|
|
164 |
if (is_bool($dataValues[$key])) {
|
|
|
165 |
$dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE';
|
|
|
166 |
} elseif ($dataValues[$key] !== null) {
|
|
|
167 |
$dataValue = $dataValues[$key];
|
|
|
168 |
// escape quotes if we have a string containing quotes
|
|
|
169 |
if (is_string($dataValue) && str_contains($dataValue, '"')) {
|
|
|
170 |
$dataValue = str_replace('"', '""', $dataValue);
|
|
|
171 |
}
|
|
|
172 |
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
|
|
|
173 |
}
|
|
|
174 |
|
|
|
175 |
return str_replace('[:' . $criterion . ']', $dataValue, $conditions);
|
|
|
176 |
}
|
|
|
177 |
}
|