Passed
Pull Request — master (#4466)
by Owen
15:22
created

Formula::convertFormula()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 10
cc 2
nc 2
nop 2
crap 2.0185
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
4
5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\DefinedName;
8
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
9
10
class Formula
11
{
12
    /** @var string[] */
13
    private array $definedNames = [];
14
15
    /**
16
     * @param DefinedName[] $definedNames
17
     */
18 40
    public function __construct(array $definedNames)
19
    {
20 40
        foreach ($definedNames as $definedName) {
21 2
            $this->definedNames[] = $definedName->getName();
22
        }
23
    }
24
25 9
    public function convertFormula(string $formula, string $worksheetName = ''): string
26
    {
27 9
        $formula = $this->convertCellReferences($formula, $worksheetName);
28 9
        $formula = $this->convertDefinedNames($formula);
29
        $formula = $this->convertFunctionNames($formula);
30 9
31
        if (!str_starts_with($formula, '=')) {
32
            $formula = '=' . $formula;
33
        }
34 9
35
        return 'of:' . $formula;
36
    }
37 9
38
    private function convertDefinedNames(string $formula): string
39 9
    {
40 9
        $splitCount = Preg::matchAllWithOffsets(
41 9
            '/' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '/mui',
42 9
            $formula,
43 9
            $splitRanges
44
        );
45 9
46 9
        $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0));
47 9
        $offsets = array_column($splitRanges[0], 1);
48
        $values = array_column($splitRanges[0], 0);
49 9
50 9
        while ($splitCount > 0) {
51 9
            --$splitCount;
52 9
            $length = $lengths[$splitCount];
53 9
            $offset = $offsets[$splitCount];
54
            $value = $values[$splitCount];
55 9
56 1
            if (in_array($value, $this->definedNames, true)) {
57
                $formula = substr($formula, 0, $offset) . '$$' . $value . substr($formula, $offset + $length);
58
            }
59
        }
60 9
61
        return $formula;
62
    }
63 9
64
    private function convertCellReferences(string $formula, string $worksheetName): string
65 9
    {
66 9
        $splitCount = Preg::matchAllWithOffsets(
67 9
            '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
68 9
            $formula,
69 9
            $splitRanges
70
        );
71 9
72 9
        $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0));
73
        $offsets = array_column($splitRanges[0], 1);
74 9
75 9
        $worksheets = $splitRanges[2];
76 9
        $columns = $splitRanges[6];
77
        $rows = $splitRanges[7];
78
79
        // Replace any commas in the formula with semi-colons for Ods
80
        // If by chance there are commas in worksheet names, then they will be "fixed" again in the loop
81 9
        //    because we've already extracted worksheet names with our Preg::matchAllWithOffsets()
82 9
        $formula = str_replace(',', ';', $formula);
83 7
        while ($splitCount > 0) {
84 7
            --$splitCount;
85 7
            $length = $lengths[$splitCount];
86 7
            $offset = $offsets[$splitCount];
87 7
            $worksheet = $worksheets[$splitCount][0];
88 7
            $column = $columns[$splitCount][0];
89
            $row = $rows[$splitCount][0];
90 7
91 7
            $newRange = '';
92 7
            if (empty($worksheet)) {
93
                if (($offset === 0) || ($formula[$offset - 1] !== ':')) {
94 7
                    // We need a worksheet
95
                    $worksheet = $worksheetName;
96
                }
97
            } else {
98
                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
99 7
            }
100
            if (!empty($worksheet)) {
101 7
                $newRange = "['" . str_replace("'", "''", $worksheet) . "'";
102 7
            } elseif (substr($formula, $offset - 1, 1) !== ':') {
103
                $newRange = '[';
104 7
            }
105
            $newRange .= '.';
106
107 7
            //if (!empty($column)) { // phpstan says always true
108
            $newRange .= $column;
109 7
            //}
110 7
            if (!empty($row)) {
111
                $newRange .= $row;
112
            }
113 7
            // close the wrapping [] unless this is the first part of a range
114
            $newRange .= substr($formula, $offset + $length, 1) !== ':' ? ']' : '';
115 7
116
            $formula = substr($formula, 0, $offset) . $newRange . substr($formula, $offset + $length);
117
        }
118 9
119
        return $formula;
120
    }
121
122
    private function convertFunctionNames(string $formula): string
123
    {
124
        return Preg::replace(
125
            [
126
                '/\b((CEILING|FLOOR)'
127
                    . '([.](MATH|PRECISE))?)\s*[(]/ui',
128
                '/\b(CEILING|FLOOR)[.]XCL\s*[(]/ui',
129
                '/\b(CEILING|FLOOR)[.]ODS\s*[(]/ui',
130
            ],
131
            [
132
                'COM.MICROSOFT.$1(',
133
                'COM.MICROSOFT.$1(',
134
                '$1(',
135
            ],
136
            $formula
137
        );
138
    }
139
}
140