You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AreaReference.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.util;
  16. import java.util.ArrayList;
  17. import java.util.List;
  18. import java.util.StringTokenizer;
  19. import org.apache.poi.ss.SpreadsheetVersion;
  20. import org.apache.poi.util.StringUtil;
  21. public class AreaReference {
  22. /** The character (!) that separates sheet names from cell references */
  23. private static final char SHEET_NAME_DELIMITER = '!';
  24. /** The character (:) that separates the two cell references in a multi-cell area reference */
  25. private static final char CELL_DELIMITER = ':';
  26. /** The character (') used to quote sheet names when they contain special characters */
  27. private static final char SPECIAL_NAME_DELIMITER = '\'';
  28. private static final SpreadsheetVersion DEFAULT_SPREADSHEET_VERSION = SpreadsheetVersion.EXCEL97;
  29. private final CellReference _firstCell;
  30. private final CellReference _lastCell;
  31. private final boolean _isSingleCell;
  32. private final SpreadsheetVersion _version; // never null
  33. /**
  34. * Create an area ref from a string representation. Sheet names containing special characters should be
  35. * delimited and escaped as per normal syntax rules for formulas.<br>
  36. * The area reference must be contiguous (i.e. represent a single rectangle, not a union of rectangles)
  37. */
  38. public AreaReference(String reference, SpreadsheetVersion version) {
  39. _version = (null != version) ? version : DEFAULT_SPREADSHEET_VERSION;
  40. if(! isContiguous(reference)) {
  41. throw new IllegalArgumentException(
  42. "References passed to the AreaReference must be contiguous, " +
  43. "use generateContiguous(ref) if you have non-contiguous references");
  44. }
  45. String[] parts = separateAreaRefs(reference);
  46. String part0 = parts[0];
  47. if (parts.length == 1) {
  48. // TODO - probably shouldn't initialize area ref when text is really a cell ref
  49. // Need to fix some named range stuff to get rid of this
  50. _firstCell = new CellReference(part0);
  51. _lastCell = _firstCell;
  52. _isSingleCell = true;
  53. return;
  54. }
  55. if (parts.length != 2) {
  56. throw new IllegalArgumentException("Bad area ref '" + reference + "'");
  57. }
  58. String part1 = parts[1];
  59. if (isPlainColumn(part0)) {
  60. if (!isPlainColumn(part1)) {
  61. throw new RuntimeException("Bad area ref '" + reference + "'");
  62. }
  63. // Special handling for whole-column references
  64. // Represented internally as x$1 to x$65536
  65. // which is the maximum range of rows
  66. boolean firstIsAbs = CellReference.isPartAbsolute(part0);
  67. boolean lastIsAbs = CellReference.isPartAbsolute(part1);
  68. int col0 = CellReference.convertColStringToIndex(part0);
  69. int col1 = CellReference.convertColStringToIndex(part1);
  70. _firstCell = new CellReference(0, col0, true, firstIsAbs);
  71. _lastCell = new CellReference(0xFFFF, col1, true, lastIsAbs);
  72. _isSingleCell = false;
  73. // TODO - whole row refs
  74. } else {
  75. _firstCell = new CellReference(part0);
  76. _lastCell = new CellReference(part1);
  77. _isSingleCell = part0.equals(part1);
  78. }
  79. }
  80. /**
  81. * Creates an area ref from a pair of Cell References.
  82. */
  83. public AreaReference(CellReference topLeft, CellReference botRight, SpreadsheetVersion version) {
  84. _version = (null != version) ? version : DEFAULT_SPREADSHEET_VERSION;
  85. boolean swapRows = topLeft.getRow() > botRight.getRow();
  86. boolean swapCols = topLeft.getCol() > botRight.getCol();
  87. if (swapRows || swapCols) {
  88. String firstSheet;
  89. String lastSheet;
  90. int firstRow;
  91. int lastRow;
  92. int firstColumn;
  93. int lastColumn;
  94. boolean firstRowAbs;
  95. boolean lastRowAbs;
  96. boolean firstColAbs;
  97. boolean lastColAbs;
  98. if (swapRows) {
  99. firstRow = botRight.getRow();
  100. firstRowAbs = botRight.isRowAbsolute();
  101. lastRow = topLeft.getRow();
  102. lastRowAbs = topLeft.isRowAbsolute();
  103. } else {
  104. firstRow = topLeft.getRow();
  105. firstRowAbs = topLeft.isRowAbsolute();
  106. lastRow = botRight.getRow();
  107. lastRowAbs = botRight.isRowAbsolute();
  108. }
  109. if (swapCols) {
  110. firstSheet = botRight.getSheetName();
  111. firstColumn = botRight.getCol();
  112. firstColAbs = botRight.isColAbsolute();
  113. lastSheet = topLeft.getSheetName();
  114. lastColumn = topLeft.getCol();
  115. lastColAbs = topLeft.isColAbsolute();
  116. } else {
  117. firstSheet = topLeft.getSheetName();
  118. firstColumn = topLeft.getCol();
  119. firstColAbs = topLeft.isColAbsolute();
  120. lastSheet = botRight.getSheetName();
  121. lastColumn = botRight.getCol();
  122. lastColAbs = botRight.isColAbsolute();
  123. }
  124. _firstCell = new CellReference(firstSheet, firstRow, firstColumn, firstRowAbs, firstColAbs);
  125. _lastCell = new CellReference(lastSheet, lastRow, lastColumn, lastRowAbs, lastColAbs);
  126. } else {
  127. _firstCell = topLeft;
  128. _lastCell = botRight;
  129. }
  130. _isSingleCell = false;
  131. }
  132. private static boolean isPlainColumn(String refPart) {
  133. for(int i=refPart.length()-1; i>=0; i--) {
  134. int ch = refPart.charAt(i);
  135. if (ch == '$' && i==0) {
  136. continue;
  137. }
  138. if (ch < 'A' || ch > 'Z') {
  139. return false;
  140. }
  141. }
  142. return true;
  143. }
  144. /**
  145. * Is the reference for a contiguous (i.e.
  146. * unbroken) area, or is it made up of
  147. * several different parts?
  148. * (If it is, you will need to call
  149. * {@link #generateContiguous(SpreadsheetVersion, String)})
  150. */
  151. public static boolean isContiguous(String reference) {
  152. return splitAreaReferences(reference).length == 1;
  153. }
  154. public static AreaReference getWholeRow(SpreadsheetVersion version, String start, String end) {
  155. if (null == version) {
  156. version = DEFAULT_SPREADSHEET_VERSION;
  157. }
  158. return new AreaReference("$A" + start + ":$" + version.getLastColumnName() + end, version);
  159. }
  160. public static AreaReference getWholeColumn(SpreadsheetVersion version, String start, String end) {
  161. if (null == version) {
  162. version = DEFAULT_SPREADSHEET_VERSION;
  163. }
  164. return new AreaReference(start + "$1:" + end + "$" + version.getMaxRows(), version);
  165. }
  166. /**
  167. * Is the reference for a whole-column reference,
  168. * such as C:C or D:G ?
  169. */
  170. public static boolean isWholeColumnReference(SpreadsheetVersion version, CellReference topLeft, CellReference botRight) {
  171. if (null == version) {
  172. version = DEFAULT_SPREADSHEET_VERSION; // how the code used to behave.
  173. }
  174. // These are represented as something like
  175. // C$1:C$65535 or D$1:F$0
  176. // i.e. absolute from 1st row to 0th one
  177. return (topLeft.getRow() == 0
  178. && topLeft.isRowAbsolute()
  179. && botRight.getRow() == version.getLastRowIndex()
  180. && botRight.isRowAbsolute());
  181. }
  182. /**
  183. * Takes a non-contiguous area reference, and returns an array of contiguous area references
  184. * @return an array of contiguous area references.
  185. */
  186. public static AreaReference[] generateContiguous(SpreadsheetVersion version, String reference) {
  187. if (null == version) {
  188. version = DEFAULT_SPREADSHEET_VERSION; // how the code used to behave.
  189. }
  190. List<AreaReference> refs = new ArrayList<>();
  191. String[] splitReferences = splitAreaReferences(reference);
  192. for (String ref : splitReferences) {
  193. refs.add(new AreaReference(ref, version));
  194. }
  195. return refs.toArray(new AreaReference[0]);
  196. }
  197. /**
  198. * Separates Area refs in two parts and returns them as separate elements in a String array,
  199. * each qualified with the sheet name (if present)
  200. *
  201. * @return array with one or two elements. never {@code null}
  202. */
  203. private static String[] separateAreaRefs(String reference) {
  204. // TODO - refactor cell reference parsing logic to one place.
  205. // Current known incarnations:
  206. // FormulaParser.GetName()
  207. // CellReference.separateRefParts()
  208. // AreaReference.separateAreaRefs() (here)
  209. // SheetNameFormatter.format() (inverse)
  210. int len = reference.length();
  211. int delimiterPos = -1;
  212. boolean insideDelimitedName = false;
  213. for(int i=0; i<len; i++) {
  214. switch(reference.charAt(i)) {
  215. case CELL_DELIMITER:
  216. if(!insideDelimitedName) {
  217. if(delimiterPos >=0) {
  218. throw new IllegalArgumentException("More than one cell delimiter '"
  219. + CELL_DELIMITER + "' appears in area reference '" + reference + "'");
  220. }
  221. delimiterPos = i;
  222. }
  223. continue; //continue the for-loop
  224. case SPECIAL_NAME_DELIMITER:
  225. break;
  226. default:
  227. continue; //continue the for-loop
  228. }
  229. if(!insideDelimitedName) {
  230. insideDelimitedName = true;
  231. continue;
  232. }
  233. if(i >= len-1) {
  234. // reference ends with the delimited name.
  235. // Assume names like: "Sheet1!'A1'" are never legal.
  236. throw new IllegalArgumentException("Area reference '" + reference
  237. + "' ends with special name delimiter '" + SPECIAL_NAME_DELIMITER + "'");
  238. }
  239. if(reference.charAt(i+1) == SPECIAL_NAME_DELIMITER) {
  240. // two consecutive quotes is the escape sequence for a single one
  241. i++; // skip this and keep parsing the special name
  242. } else {
  243. // this is the end of the delimited name
  244. insideDelimitedName = false;
  245. }
  246. }
  247. if(delimiterPos < 0) {
  248. return new String[] { reference, };
  249. }
  250. String partA = reference.substring(0, delimiterPos);
  251. String partB = reference.substring(delimiterPos+1);
  252. if(partB.indexOf(SHEET_NAME_DELIMITER) >= 0) {
  253. // partB contains SHEET_NAME_DELIMITER
  254. // TODO - are references like "Sheet1!A1:Sheet1:B2" ever valid?
  255. // FormulaParser has code to handle that.
  256. throw new RuntimeException("Unexpected " + SHEET_NAME_DELIMITER
  257. + " in second cell reference of '" + reference + "'");
  258. }
  259. int plingPos = partA.lastIndexOf(SHEET_NAME_DELIMITER);
  260. if(plingPos < 0) {
  261. return new String [] { partA, partB, };
  262. }
  263. String sheetName = partA.substring(0, plingPos + 1); // +1 to include delimiter
  264. return new String [] { partA, sheetName + partB, };
  265. }
  266. /**
  267. * Splits a comma-separated area references string into an array of
  268. * individual references
  269. * @param reference Area references, i.e. A1:B2, 'Sheet1'!A1:B2
  270. * @return Area references in an array, size >= 1
  271. */
  272. private static String[] splitAreaReferences(String reference) {
  273. List<String> results = new ArrayList<>();
  274. String currentSegment = "";
  275. StringTokenizer st = new StringTokenizer(reference, ",");
  276. while(st.hasMoreTokens()) {
  277. if (currentSegment.length() > 0) {
  278. currentSegment += ",";
  279. }
  280. currentSegment += st.nextToken();
  281. int numSingleQuotes = StringUtil.countMatches(currentSegment, '\'');
  282. if (numSingleQuotes == 0 || numSingleQuotes == 2) {
  283. results.add(currentSegment);
  284. currentSegment = "";
  285. }
  286. }
  287. if (currentSegment.length() > 0) {
  288. results.add(currentSegment);
  289. }
  290. return results.toArray(new String[0]);
  291. }
  292. public boolean isWholeColumnReference() {
  293. return isWholeColumnReference(_version, _firstCell, _lastCell);
  294. }
  295. /**
  296. * @return {@code false} if this area reference involves more than one cell
  297. */
  298. public boolean isSingleCell() {
  299. return _isSingleCell;
  300. }
  301. /**
  302. * @return the first cell reference which defines this area. Usually this cell is in the upper
  303. * left corner of the area (but this is not a requirement).
  304. */
  305. public CellReference getFirstCell() {
  306. return _firstCell;
  307. }
  308. /**
  309. * Note - if this area reference refers to a single cell, the return value of this method will
  310. * be identical to that of {@code getFirstCell()}
  311. * @return the second cell reference which defines this area. For multi-cell areas, this is
  312. * cell diagonally opposite the 'first cell'. Usually this cell is in the lower right corner
  313. * of the area (but this is not a requirement).
  314. */
  315. public CellReference getLastCell() {
  316. return _lastCell;
  317. }
  318. /**
  319. * Returns a reference to every cell covered by this area
  320. */
  321. public CellReference[] getAllReferencedCells() {
  322. // Special case for single cell reference
  323. if(_isSingleCell) {
  324. return new CellReference[] { _firstCell, };
  325. }
  326. // Interpolate between the two
  327. int minRow = Math.min(_firstCell.getRow(), _lastCell.getRow());
  328. int maxRow = Math.max(_firstCell.getRow(), _lastCell.getRow());
  329. int minCol = Math.min(_firstCell.getCol(), _lastCell.getCol());
  330. int maxCol = Math.max(_firstCell.getCol(), _lastCell.getCol());
  331. String sheetName = _firstCell.getSheetName();
  332. List<CellReference> refs = new ArrayList<>();
  333. for(int row=minRow; row<=maxRow; row++) {
  334. for(int col=minCol; col<=maxCol; col++) {
  335. CellReference ref = new CellReference(sheetName, row, col, _firstCell.isRowAbsolute(), _firstCell.isColAbsolute());
  336. refs.add(ref);
  337. }
  338. }
  339. return refs.toArray(new CellReference[0]);
  340. }
  341. /**
  342. * Returns a text representation of this area reference.
  343. * <p>
  344. * Example return values:
  345. * <table>
  346. * <caption>Example return values</caption>
  347. * <tr><th align='left'>Result</th><th align='left'>Comment</th></tr>
  348. * <tr><td>A1:A1</td><td>Single cell area reference without sheet</td></tr>
  349. * <tr><td>A1:$C$1</td><td>Multi-cell area reference without sheet</td></tr>
  350. * <tr><td>Sheet1!A$1:B4</td><td>Standard sheet name</td></tr>
  351. * <tr><td>'O''Brien''s Sales'!B5:C6'&nbsp;</td><td>Sheet name with special characters</td></tr>
  352. * </table>
  353. * @return the text representation of this area reference as it would appear in a formula.
  354. */
  355. public String formatAsString() {
  356. // Special handling for whole-column references
  357. if(isWholeColumnReference()) {
  358. return
  359. CellReference.convertNumToColString(_firstCell.getCol())
  360. + ":" +
  361. CellReference.convertNumToColString(_lastCell.getCol());
  362. }
  363. StringBuilder sb = new StringBuilder(32);
  364. sb.append(_firstCell.formatAsString());
  365. if(!_isSingleCell) {
  366. sb.append(CELL_DELIMITER);
  367. if(_lastCell.getSheetName() == null) {
  368. sb.append(_lastCell.formatAsString());
  369. } else {
  370. // don't want to include the sheet name twice
  371. _lastCell.appendCellReference(sb);
  372. }
  373. }
  374. return sb.toString();
  375. }
  376. public String toString() {
  377. StringBuilder sb = new StringBuilder(64);
  378. sb.append(getClass().getName()).append(" [");
  379. try {
  380. sb.append(formatAsString());
  381. } catch(Exception e) {
  382. sb.append(e);
  383. }
  384. sb.append(']');
  385. return sb.toString();
  386. }
  387. }