Coordinate.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Cell;
  3. use PhpOffice\PhpSpreadsheet\Exception;
  4. /**
  5. * Helper class to manipulate cell coordinates.
  6. *
  7. * Columns indexes and rows are always based on 1, **not** on 0. This match the behavior
  8. * that Excel users are used to, and also match the Excel functions `COLUMN()` and `ROW()`.
  9. */
  10. abstract class Coordinate
  11. {
  12. /**
  13. * Default range variable constant.
  14. *
  15. * @var string
  16. */
  17. const DEFAULT_RANGE = 'A1:A1';
  18. /**
  19. * Coordinate from string.
  20. *
  21. * @param string $pCoordinateString eg: 'A1'
  22. *
  23. * @throws Exception
  24. *
  25. * @return string[] Array containing column and row (indexes 0 and 1)
  26. */
  27. public static function coordinateFromString($pCoordinateString)
  28. {
  29. if (preg_match('/^([$]?[A-Z]{1,3})([$]?\\d{1,7})$/', $pCoordinateString, $matches)) {
  30. return [$matches[1], $matches[2]];
  31. } elseif (self::coordinateIsRange($pCoordinateString)) {
  32. throw new Exception('Cell coordinate string can not be a range of cells');
  33. } elseif ($pCoordinateString == '') {
  34. throw new Exception('Cell coordinate can not be zero-length string');
  35. }
  36. throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
  37. }
  38. /**
  39. * Checks if a coordinate represents a range of cells.
  40. *
  41. * @param string $coord eg: 'A1' or 'A1:A2' or 'A1:A2,C1:C2'
  42. *
  43. * @return bool Whether the coordinate represents a range of cells
  44. */
  45. public static function coordinateIsRange($coord)
  46. {
  47. return (strpos($coord, ':') !== false) || (strpos($coord, ',') !== false);
  48. }
  49. /**
  50. * Make string row, column or cell coordinate absolute.
  51. *
  52. * @param string $pCoordinateString e.g. 'A' or '1' or 'A1'
  53. * Note that this value can be a row or column reference as well as a cell reference
  54. *
  55. * @throws Exception
  56. *
  57. * @return string Absolute coordinate e.g. '$A' or '$1' or '$A$1'
  58. */
  59. public static function absoluteReference($pCoordinateString)
  60. {
  61. if (self::coordinateIsRange($pCoordinateString)) {
  62. throw new Exception('Cell coordinate string can not be a range of cells');
  63. }
  64. // Split out any worksheet name from the reference
  65. $worksheet = '';
  66. $cellAddress = explode('!', $pCoordinateString);
  67. if (count($cellAddress) > 1) {
  68. list($worksheet, $pCoordinateString) = $cellAddress;
  69. }
  70. if ($worksheet > '') {
  71. $worksheet .= '!';
  72. }
  73. // Create absolute coordinate
  74. if (ctype_digit($pCoordinateString)) {
  75. return $worksheet . '$' . $pCoordinateString;
  76. } elseif (ctype_alpha($pCoordinateString)) {
  77. return $worksheet . '$' . strtoupper($pCoordinateString);
  78. }
  79. return $worksheet . self::absoluteCoordinate($pCoordinateString);
  80. }
  81. /**
  82. * Make string coordinate absolute.
  83. *
  84. * @param string $pCoordinateString e.g. 'A1'
  85. *
  86. * @throws Exception
  87. *
  88. * @return string Absolute coordinate e.g. '$A$1'
  89. */
  90. public static function absoluteCoordinate($pCoordinateString)
  91. {
  92. if (self::coordinateIsRange($pCoordinateString)) {
  93. throw new Exception('Cell coordinate string can not be a range of cells');
  94. }
  95. // Split out any worksheet name from the coordinate
  96. $worksheet = '';
  97. $cellAddress = explode('!', $pCoordinateString);
  98. if (count($cellAddress) > 1) {
  99. list($worksheet, $pCoordinateString) = $cellAddress;
  100. }
  101. if ($worksheet > '') {
  102. $worksheet .= '!';
  103. }
  104. // Create absolute coordinate
  105. list($column, $row) = self::coordinateFromString($pCoordinateString);
  106. $column = ltrim($column, '$');
  107. $row = ltrim($row, '$');
  108. return $worksheet . '$' . $column . '$' . $row;
  109. }
  110. /**
  111. * Split range into coordinate strings.
  112. *
  113. * @param string $pRange e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
  114. *
  115. * @return array Array containg one or more arrays containing one or two coordinate strings
  116. * e.g. array('B4','D9') or array(array('B4','D9'),array('H2','O11'))
  117. * or array('B4')
  118. */
  119. public static function splitRange($pRange)
  120. {
  121. // Ensure $pRange is a valid range
  122. if (empty($pRange)) {
  123. $pRange = self::DEFAULT_RANGE;
  124. }
  125. $exploded = explode(',', $pRange);
  126. $counter = count($exploded);
  127. for ($i = 0; $i < $counter; ++$i) {
  128. $exploded[$i] = explode(':', $exploded[$i]);
  129. }
  130. return $exploded;
  131. }
  132. /**
  133. * Build range from coordinate strings.
  134. *
  135. * @param array $pRange Array containg one or more arrays containing one or two coordinate strings
  136. *
  137. * @throws Exception
  138. *
  139. * @return string String representation of $pRange
  140. */
  141. public static function buildRange(array $pRange)
  142. {
  143. // Verify range
  144. if (empty($pRange) || !is_array($pRange[0])) {
  145. throw new Exception('Range does not contain any information');
  146. }
  147. // Build range
  148. $imploded = [];
  149. $counter = count($pRange);
  150. for ($i = 0; $i < $counter; ++$i) {
  151. $pRange[$i] = implode(':', $pRange[$i]);
  152. }
  153. $imploded = implode(',', $pRange);
  154. return $imploded;
  155. }
  156. /**
  157. * Calculate range boundaries.
  158. *
  159. * @param string $pRange Cell range (e.g. A1:A1)
  160. *
  161. * @return array Range coordinates array(Start Cell, End Cell)
  162. * where Start Cell and End Cell are arrays (Column Number, Row Number)
  163. */
  164. public static function rangeBoundaries($pRange)
  165. {
  166. // Ensure $pRange is a valid range
  167. if (empty($pRange)) {
  168. $pRange = self::DEFAULT_RANGE;
  169. }
  170. // Uppercase coordinate
  171. $pRange = strtoupper($pRange);
  172. // Extract range
  173. if (strpos($pRange, ':') === false) {
  174. $rangeA = $rangeB = $pRange;
  175. } else {
  176. list($rangeA, $rangeB) = explode(':', $pRange);
  177. }
  178. // Calculate range outer borders
  179. $rangeStart = self::coordinateFromString($rangeA);
  180. $rangeEnd = self::coordinateFromString($rangeB);
  181. // Translate column into index
  182. $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
  183. $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
  184. return [$rangeStart, $rangeEnd];
  185. }
  186. /**
  187. * Calculate range dimension.
  188. *
  189. * @param string $pRange Cell range (e.g. A1:A1)
  190. *
  191. * @return array Range dimension (width, height)
  192. */
  193. public static function rangeDimension($pRange)
  194. {
  195. // Calculate range outer borders
  196. list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
  197. return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
  198. }
  199. /**
  200. * Calculate range boundaries.
  201. *
  202. * @param string $pRange Cell range (e.g. A1:A1)
  203. *
  204. * @return array Range coordinates array(Start Cell, End Cell)
  205. * where Start Cell and End Cell are arrays (Column ID, Row Number)
  206. */
  207. public static function getRangeBoundaries($pRange)
  208. {
  209. // Ensure $pRange is a valid range
  210. if (empty($pRange)) {
  211. $pRange = self::DEFAULT_RANGE;
  212. }
  213. // Uppercase coordinate
  214. $pRange = strtoupper($pRange);
  215. // Extract range
  216. if (strpos($pRange, ':') === false) {
  217. $rangeA = $rangeB = $pRange;
  218. } else {
  219. list($rangeA, $rangeB) = explode(':', $pRange);
  220. }
  221. return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
  222. }
  223. /**
  224. * Column index from string.
  225. *
  226. * @param string $pString eg 'A'
  227. *
  228. * @return int Column index (A = 1)
  229. */
  230. public static function columnIndexFromString($pString)
  231. {
  232. // Using a lookup cache adds a slight memory overhead, but boosts speed
  233. // caching using a static within the method is faster than a class static,
  234. // though it's additional memory overhead
  235. static $indexCache = [];
  236. if (isset($indexCache[$pString])) {
  237. return $indexCache[$pString];
  238. }
  239. // It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
  240. // and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
  241. // memory overhead either
  242. static $columnLookup = [
  243. 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
  244. 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
  245. 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
  246. 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
  247. ];
  248. // We also use the language construct isset() rather than the more costly strlen() function to match the length of $pString
  249. // for improved performance
  250. if (isset($pString[0])) {
  251. if (!isset($pString[1])) {
  252. $indexCache[$pString] = $columnLookup[$pString];
  253. return $indexCache[$pString];
  254. } elseif (!isset($pString[2])) {
  255. $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];
  256. return $indexCache[$pString];
  257. } elseif (!isset($pString[3])) {
  258. $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];
  259. return $indexCache[$pString];
  260. }
  261. }
  262. throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
  263. }
  264. /**
  265. * String from column index.
  266. *
  267. * @param int $columnIndex Column index (A = 1)
  268. *
  269. * @return string
  270. */
  271. public static function stringFromColumnIndex($columnIndex)
  272. {
  273. static $indexCache = [];
  274. if (!isset($indexCache[$columnIndex])) {
  275. $indexValue = $columnIndex;
  276. $base26 = null;
  277. do {
  278. $characterValue = ($indexValue % 26) ?: 26;
  279. $indexValue = ($indexValue - $characterValue) / 26;
  280. $base26 = chr($characterValue + 64) . ($base26 ?: '');
  281. } while ($indexValue > 0);
  282. $indexCache[$columnIndex] = $base26;
  283. }
  284. return $indexCache[$columnIndex];
  285. }
  286. /**
  287. * Extract all cell references in range.
  288. *
  289. * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
  290. *
  291. * @return array Array containing single cell references
  292. */
  293. public static function extractAllCellReferencesInRange($pRange)
  294. {
  295. // Returnvalue
  296. $returnValue = [];
  297. // Explode spaces
  298. $cellBlocks = explode(' ', str_replace('$', '', strtoupper($pRange)));
  299. foreach ($cellBlocks as $cellBlock) {
  300. // Single cell?
  301. if (!self::coordinateIsRange($cellBlock)) {
  302. $returnValue[] = $cellBlock;
  303. continue;
  304. }
  305. // Range...
  306. $ranges = self::splitRange($cellBlock);
  307. foreach ($ranges as $range) {
  308. // Single cell?
  309. if (!isset($range[1])) {
  310. $returnValue[] = $range[0];
  311. continue;
  312. }
  313. // Range...
  314. list($rangeStart, $rangeEnd) = $range;
  315. sscanf($rangeStart, '%[A-Z]%d', $startCol, $startRow);
  316. sscanf($rangeEnd, '%[A-Z]%d', $endCol, $endRow);
  317. ++$endCol;
  318. // Current data
  319. $currentCol = $startCol;
  320. $currentRow = $startRow;
  321. // Loop cells
  322. while ($currentCol != $endCol) {
  323. while ($currentRow <= $endRow) {
  324. $returnValue[] = $currentCol . $currentRow;
  325. ++$currentRow;
  326. }
  327. ++$currentCol;
  328. $currentRow = $startRow;
  329. }
  330. }
  331. }
  332. // Sort the result by column and row
  333. $sortKeys = [];
  334. foreach (array_unique($returnValue) as $coord) {
  335. sscanf($coord, '%[A-Z]%d', $column, $row);
  336. $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
  337. }
  338. ksort($sortKeys);
  339. // Return value
  340. return array_values($sortKeys);
  341. }
  342. /**
  343. * Convert an associative array of single cell coordinates to values to an associative array
  344. * of cell ranges to values. Only adjacent cell coordinates with the same
  345. * value will be merged. If the value is an object, it must implement the method getHashCode().
  346. *
  347. * For example, this function converts:
  348. *
  349. * [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
  350. *
  351. * to:
  352. *
  353. * [ 'A1:A3' => 'x', 'A4' => 'y' ]
  354. *
  355. * @param array $pCoordCollection associative array mapping coordinates to values
  356. *
  357. * @return array associative array mapping coordinate ranges to valuea
  358. */
  359. public static function mergeRangesInCollection(array $pCoordCollection)
  360. {
  361. $hashedValues = [];
  362. $mergedCoordCollection = [];
  363. foreach ($pCoordCollection as $coord => $value) {
  364. if (self::coordinateIsRange($coord)) {
  365. $mergedCoordCollection[$coord] = $value;
  366. continue;
  367. }
  368. list($column, $row) = self::coordinateFromString($coord);
  369. $row = (int) (ltrim($row, '$'));
  370. $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);
  371. if (!isset($hashedValues[$hashCode])) {
  372. $hashedValues[$hashCode] = (object) [
  373. 'value' => $value,
  374. 'col' => $column,
  375. 'rows' => [$row],
  376. ];
  377. } else {
  378. $hashedValues[$hashCode]->rows[] = $row;
  379. }
  380. }
  381. ksort($hashedValues);
  382. foreach ($hashedValues as $hashedValue) {
  383. sort($hashedValue->rows);
  384. $rowStart = null;
  385. $rowEnd = null;
  386. $ranges = [];
  387. foreach ($hashedValue->rows as $row) {
  388. if ($rowStart === null) {
  389. $rowStart = $row;
  390. $rowEnd = $row;
  391. } elseif ($rowEnd === $row - 1) {
  392. $rowEnd = $row;
  393. } else {
  394. if ($rowStart == $rowEnd) {
  395. $ranges[] = $hashedValue->col . $rowStart;
  396. } else {
  397. $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
  398. }
  399. $rowStart = $row;
  400. $rowEnd = $row;
  401. }
  402. }
  403. if ($rowStart !== null) {
  404. if ($rowStart == $rowEnd) {
  405. $ranges[] = $hashedValue->col . $rowStart;
  406. } else {
  407. $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
  408. }
  409. }
  410. foreach ($ranges as $range) {
  411. $mergedCoordCollection[$range] = $hashedValue->value;
  412. }
  413. }
  414. return $mergedCoordCollection;
  415. }
  416. }