Passed
Push — master ( a497b6...a58f3f )
by Marcel
04:33 queued 14s
created

LocalExcel::convertExcelDate()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 54
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 38
nc 6
nop 3
dl 0
loc 54
rs 8.6897
c 1
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Analytics
4
 *
5
 * SPDX-FileCopyrightText: 2019-2022 Marcel Scherello
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8
9
namespace OCA\Analytics\Datasource;
10
11
use OCP\Files\IRootFolder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\IRootFolder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use OCP\Files\NotFoundException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use OCP\IL10N;
0 ignored issues
show
Bug introduced by
The type OCP\IL10N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
15
use PhpOffice\PhpSpreadsheet\IOFactory;
16
use PhpOffice\PhpSpreadsheet\Reader\Exception;
17
use PhpOffice\PhpSpreadsheet\Shared\Date;
18
use Psr\Log\LoggerInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Log\LoggerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
20
class LocalExcel implements IDatasource {
21
	private $logger;
22
	private $rootFolder;
23
	private $l10n;
24
25
	public function __construct(
26
		IL10N           $l10n,
27
		LoggerInterface $logger,
28
		IRootFolder     $rootFolder
29
	) {
30
		$this->l10n = $l10n;
31
		$this->logger = $logger;
32
		$this->rootFolder = $rootFolder;
33
	}
34
35
	/**
36
	 * @return string Display Name of the datasource
37
	 */
38
	public function getName(): string {
39
		return $this->l10n->t('Local') . ': Spreadsheet';
40
	}
41
42
	/**
43
	 * @return int digit unique datasource id
44
	 */
45
	public function getId(): int {
46
		return 7;
47
	}
48
49
	/**
50
	 * @return array available options of the data source
51
	 */
52
	public function getTemplate(): array {
53
		$template = array();
54
		$template[] = [
55
			'id' => 'link',
56
			'name' => $this->l10n->t('File'),
57
			'placeholder' => $this->l10n->t('File'),
58
			'type' => 'filePicker'
59
		];
60
		$template[] = [
61
			'id' => 'sheet',
62
			'name' => $this->l10n->t('Sheet'),
63
			'placeholder' => $this->l10n->t('sheet name')
64
		];
65
		$template[] = [
66
			'id' => 'range',
67
			'name' => $this->l10n->t('Cell range'),
68
			'placeholder' => $this->l10n->t('e.g. A1:C3,A5:C5')
69
		];
70
		return $template;
71
	}
72
73
	/**
74
	 * Read the Data
75
	 * @param $option
76
	 * @return array
77
	 * @throws NotFoundException
78
	 * @throws \OCP\Files\NotPermittedException
79
	 * @throws Exception
80
	 * @throws \PhpOffice\PhpSpreadsheet\Exception
81
	 */
82
	public function readData($option): array {
83
		include_once __DIR__ . '/../../vendor/autoload.php';
84
		$header = $dataClean = $data = array();
85
		$headerrow = $error = 0;
86
87
		$file = $this->rootFolder->getUserFolder($option['user_id'])->get($option['link']);
88
		$fileName = $file->getStorage()->getLocalFile($file->getInternalPath());
89
90
		$inputFileType = IOFactory::identify($fileName);
91
		$reader = IOFactory::createReader($inputFileType);
92
		//$reader->setReadDataOnly(true); disabled as idDate is not working otherwise
93
		if (strlen($option['sheet']) > 0) {
94
			$reader->setLoadSheetsOnly([$option['sheet']]);
95
		}
96
		$spreadsheet = $reader->load($fileName);
97
98
		// separated columns can be selected via ranges e.g. "A1:B9,C1:C9"
99
		// these ranges are read and linked
100
		$ranges = str_getcsv($option['range']);
101
		foreach ($ranges as $range) {
102
			$values = $spreadsheet->getActiveSheet()
103
								  ->rangeToArray($range, // The worksheet range that we want to retrieve
104
									  null,         // Value that should be returned for empty cells
105
									  true,   // Should formulas be calculated (the equivalent of getCalculatedValue() for each cell)
106
									  true,        // Should values be formatted (the equivalent of getFormattedValue() for each cell)
107
									  false       // Should the array be indexed by cell row and cell column
108
								  );
109
110
			$values = $this->convertExcelDate($spreadsheet, $values, $range);
111
112
			if (empty($data)) {
113
				// first range will fill the array with all rows
114
				$data = $values;
115
			} else {
116
				// further columns will be attached to the first ones
117
				foreach ($data as $key => $value) {
118
					$data[$key] = array_merge($value, $values[$key]);
119
				}
120
			}
121
		}
122
123
		// Remove columns that are completely null
124
		$data = $this->removeNullColumns($data);
125
126
		foreach ($data as $key => $value) {
127
			if ($headerrow === 0) {
128
				$header = array_values($value);
129
				$headerrow = 1;
130
			} else if (!$this->containsOnlyNull($value)) {
131
				array_push($dataClean, array_values($value));
132
			}
133
		}
134
135
		return [
136
			'header' => $header,
137
			'dimensions' => array_slice($header, 0, count($header) - 1),
138
			'data' => $dataClean,
139
			'error' => $error,
140
		];
141
	}
142
143
	private function removeNullColumns(array $data): array {
144
		// Transpose the data to work with columns as rows
145
		$transposedData = array_map(null, ...$data);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type callable expected by parameter $callback of array_map(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

145
		$transposedData = array_map(/** @scrutinizer ignore-type */ null, ...$data);
Loading history...
146
147
		// Filter out columns that contain only null values
148
		$filteredData = array_filter($transposedData, function ($column) {
149
			return !$this->containsOnlyNull($column);
150
		});
151
152
		// Transpose back to original row-column structure
153
		return array_map(null, ...$filteredData);
154
	}
155
156
	/**
157
	 * @param $array
158
	 * @return bool
159
	 */
160
	private function containsOnlyNull($array): bool {
161
		return !(array_reduce($array, function ($carry, $item) {
162
				return $carry += (is_null($item) ? 0 : 1);
163
			}, 0) > 0);
164
	}
165
166
	/**
167
	 * every cell is checked if it is an excel date (stored in number of days since 1990)
168
	 * then, the date format from excel is reapplied in php
169
	 * @param $spreadsheet
170
	 * @param $values
171
	 * @param $range
172
	 * @return array
173
	 * @throws \PhpOffice\PhpSpreadsheet\Exception
174
	 */
175
	private function convertExcelDate($spreadsheet, $values, $range): array {
176
		$map = [
177
			"yyyy" => "Y",  // Four-digit year
178
			"yy" => "y",    // Two-digit year
179
			"MM" => "m",    // Two-digit month
180
			"mm" => "i",    // Two-digit minutes (lowercase)
181
			"dd" => "d",    // Two-digit day
182
			"d" => "j",     // Day without leading zeros
183
			"hh" => "H",    // 24-hour format
184
			"h" => "G",     // 12-hour format
185
			"ss" => "s"     // Seconds
186
		];
187
188
		$start = str_getcsv($range, ':');
189
		$startCell = Coordinate::coordinateFromString($start[0]);
190
		$startColumn = (int)Coordinate::columnIndexFromString($startCell[0]);
191
		$startRow = (int)$startCell[1];
192
193
		foreach ($values as $rowIndex => $row) {
194
			foreach ($row as $columnIndex => $cellValue) {
195
				$columnLetter = Coordinate::stringFromColumnIndex($columnIndex + $startColumn);
196
				$rowNumber = $rowIndex + $startRow;
197
				$coordinate = $columnLetter . $rowNumber;
198
				$cell = $spreadsheet->getActiveSheet()->getCell($coordinate);
199
				$excelFormat = $cell->getStyle()->getNumberFormat()->getFormatCode();
200
201
				if (preg_match('/%/', $excelFormat)) {
202
					// Convert percentage to decimal value
203
					$cellValue = $cell->getCalculatedValue();
204
					$values[$rowIndex][$columnIndex] = round($cellValue, 2);
205
				} elseif (Date::isDateTime($cell)) {
206
					$excelFormat = rtrim($excelFormat, ";@");
207
208
					// Check if it's a duration format (e.g., h:mm, [h]:mm, h:mm:ss)
209
					if (preg_match('/[h]+:?[m]+:?[s]*/i', $excelFormat)) {
210
						// Convert time duration to decimal
211
						$excelTime = $cell->getCalculatedValue();
212
						$totalHours = Date::excelToDateTimeObject($excelTime)->format('G'); // Extract hours
213
						$totalMinutes = Date::excelToDateTimeObject($excelTime)->format('i'); // Extract minutes
214
						$totalSeconds = Date::excelToDateTimeObject($excelTime)->format('s'); // Extract seconds
215
216
						// Convert the time to decimal (minutes)
217
						$totalMinutesValue = ($totalHours * 60) + $totalMinutes + ($totalSeconds / 60);
218
						$values[$rowIndex][$columnIndex] = round($totalMinutesValue, 2); // Rounded to 2 decimal places
219
					} else {
220
						// Regular date formatting
221
						$date = Date::excelToDateTimeObject($cell->getCalculatedValue());
222
						$targetFormat = strtr($excelFormat, $map);
223
						$values[$rowIndex][$columnIndex] = $date->format($targetFormat);
224
					}
225
				}
226
			}
227
		}
228
		return $values;
229
	}
230
}