| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| ==================================================================== */ |
| |
| package org.apache.poi.ss.util; |
| |
| import java.util.EnumSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| import java.util.function.Supplier; |
| |
| import org.apache.poi.common.Duplicatable; |
| import org.apache.poi.common.usermodel.GenericRecord; |
| import org.apache.poi.ss.SpreadsheetVersion; |
| import org.apache.poi.ss.usermodel.Cell; |
| import org.apache.poi.util.GenericRecordUtil; |
| |
| |
| /** |
| * See OOO documentation: excelfileformat.pdf sec 2.5.14 - 'Cell Range Address'<p> |
| * |
| * Common superclass of 8-bit and 16-bit versions |
| */ |
| public abstract class CellRangeAddressBase implements Iterable<CellAddress>, Duplicatable, GenericRecord { |
| |
| /** |
| * Indicates a cell or range is in the given relative position in a range. |
| * More than one of these may apply at once. |
| */ |
| public enum CellPosition { |
| /** range starting rows are equal */ |
| TOP, |
| /** range ending rows are equal */ |
| BOTTOM, |
| /** range starting columns are equal */ |
| LEFT, |
| /** range ending columns are equal */ |
| RIGHT, |
| /** a cell or range is completely inside another range, without touching any edges (a cell in this position can't be in any others) */ |
| INSIDE, |
| ; |
| } |
| private int _firstRow; |
| private int _firstCol; |
| private int _lastRow; |
| private int _lastCol; |
| |
| protected CellRangeAddressBase(int firstRow, int lastRow, int firstCol, int lastCol) { |
| _firstRow = firstRow; |
| _lastRow = lastRow; |
| _firstCol = firstCol; |
| _lastCol = lastCol; |
| } |
| |
| /** |
| * Validate the range limits against the supplied version of Excel |
| * |
| * @param ssVersion the version of Excel to validate against |
| * @throws IllegalArgumentException if the range limits are outside of the allowed range |
| */ |
| public void validate(SpreadsheetVersion ssVersion) { |
| validateRow(_firstRow, ssVersion); |
| validateRow(_lastRow, ssVersion); |
| validateColumn(_firstCol, ssVersion); |
| validateColumn(_lastCol, ssVersion); |
| } |
| /** |
| * Runs a bounds check for row numbers |
| * @param row |
| */ |
| private static void validateRow(int row, SpreadsheetVersion ssVersion) { |
| int maxrow = ssVersion.getLastRowIndex(); |
| if (row > maxrow) throw new IllegalArgumentException("Maximum row number is " + maxrow); |
| if (row < 0) throw new IllegalArgumentException("Minumum row number is 0"); |
| } |
| |
| /** |
| * Runs a bounds check for column numbers |
| * @param column |
| */ |
| private static void validateColumn(int column, SpreadsheetVersion ssVersion) { |
| int maxcol = ssVersion.getLastColumnIndex(); |
| if (column > maxcol) throw new IllegalArgumentException("Maximum column number is " + maxcol); |
| if (column < 0) throw new IllegalArgumentException("Minimum column number is 0"); |
| } |
| |
| |
| //TODO use the correct SpreadsheetVersion |
| public final boolean isFullColumnRange() { |
| return (_firstRow == 0 && _lastRow == SpreadsheetVersion.EXCEL97.getLastRowIndex()) |
| || (_firstRow == -1 && _lastRow == -1); |
| } |
| //TODO use the correct SpreadsheetVersion |
| public final boolean isFullRowRange() { |
| return (_firstCol == 0 && _lastCol == SpreadsheetVersion.EXCEL97.getLastColumnIndex()) |
| || (_firstCol == -1 && _lastCol == -1); |
| } |
| |
| /** |
| * @return column number for the upper left hand corner |
| */ |
| public final int getFirstColumn() { |
| return _firstCol; |
| } |
| |
| /** |
| * @return row number for the upper left hand corner |
| */ |
| public final int getFirstRow() { |
| return _firstRow; |
| } |
| |
| /** |
| * @return column number for the lower right hand corner |
| */ |
| public final int getLastColumn() { |
| return _lastCol; |
| } |
| |
| /** |
| * @return row number for the lower right hand corner |
| */ |
| public final int getLastRow() { |
| return _lastRow; |
| } |
| |
| /** |
| * Determines if the given coordinates lie within the bounds |
| * of this range. |
| * |
| * @param rowInd The row, 0-based. |
| * @param colInd The column, 0-based. |
| * @return True if the coordinates lie within the bounds, false otherwise. |
| * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap |
| */ |
| public boolean isInRange(int rowInd, int colInd) { |
| return _firstRow <= rowInd && rowInd <= _lastRow && //containsRow |
| _firstCol <= colInd && colInd <= _lastCol; //containsColumn |
| } |
| |
| /** |
| * Determines if the given {@link CellReference} lies within the bounds |
| * of this range. |
| * <p>NOTE: It is up to the caller to ensure the reference is |
| * for the correct sheet, since this instance doesn't have a sheet reference. |
| * |
| * @param ref the CellReference to check |
| * @return True if the reference lies within the bounds, false otherwise. |
| * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap |
| */ |
| public boolean isInRange(CellReference ref) { |
| return isInRange(ref.getRow(), ref.getCol()); |
| } |
| |
| /** |
| * Determines if the given {@link CellAddress} lies within the bounds |
| * of this range. |
| * <p>NOTE: It is up to the caller to ensure the reference is |
| * for the correct sheet, since this instance doesn't have a sheet reference. |
| * |
| * @param ref the CellAddress to check |
| * @return True if the reference lies within the bounds, false otherwise. |
| * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap |
| */ |
| public boolean isInRange(CellAddress ref) { |
| return isInRange(ref.getRow(), ref.getColumn()); |
| } |
| |
| /** |
| * Determines if the given {@link Cell} lies within the bounds |
| * of this range. |
| * <p>NOTE: It is up to the caller to ensure the reference is |
| * for the correct sheet, since this instance doesn't have a sheet reference. |
| * |
| * @param cell the Cell to check |
| * @return True if the cell lies within the bounds, false otherwise. |
| * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap |
| */ |
| public boolean isInRange(Cell cell) { |
| return isInRange(cell.getRowIndex(), cell.getColumnIndex()); |
| } |
| |
| /** |
| * Check if the row is in the specified cell range |
| * |
| * @param rowInd the row to check |
| * @return true if the range contains the row [rowInd] |
| */ |
| public boolean containsRow(int rowInd) { |
| return _firstRow <= rowInd && rowInd <= _lastRow; |
| } |
| |
| /** |
| * Check if the column is in the specified cell range |
| * |
| * @param colInd the column to check |
| * @return true if the range contains the column [colInd] |
| */ |
| public boolean containsColumn(int colInd) { |
| return _firstCol <= colInd && colInd <= _lastCol; |
| } |
| |
| /** |
| * Determines whether or not this CellRangeAddress and the specified CellRangeAddress intersect. |
| * |
| * @param other a candidate cell range address to check for intersection with this range |
| * @return returns true if this range and other range have at least 1 cell in common |
| * @see #isInRange(int, int) for checking if a single cell intersects |
| */ |
| public boolean intersects(CellRangeAddressBase other) { |
| return this._firstRow <= other._lastRow && |
| this._firstCol <= other._lastCol && |
| other._firstRow <= this._lastRow && |
| other._firstCol <= this._lastCol; |
| } |
| |
| /** |
| * Useful for logic like table/range styling, where some elements apply based on relative position in a range. |
| * @param rowInd |
| * @param colInd |
| * @return set of {@link CellPosition}s occupied by the given coordinates. Empty if the coordinates are not in the range, never null. |
| * @since 3.17 beta 1 |
| */ |
| public Set<CellPosition> getPosition(int rowInd, int colInd) { |
| Set<CellPosition> positions = EnumSet.noneOf(CellPosition.class); |
| if (rowInd > getFirstRow() && rowInd < getLastRow() && colInd > getFirstColumn() && colInd < getLastColumn()) { |
| positions.add(CellPosition.INSIDE); |
| return positions; // entirely inside, matches no boundaries |
| } |
| // check edges |
| if (rowInd == getFirstRow()) positions.add(CellPosition.TOP); |
| if (rowInd == getLastRow()) positions.add(CellPosition.BOTTOM); |
| if (colInd == getFirstColumn()) positions.add(CellPosition.LEFT); |
| if (colInd == getLastColumn()) positions.add(CellPosition.RIGHT); |
| |
| return positions; |
| } |
| |
| /** |
| * @param firstCol column number for the upper left hand corner |
| */ |
| public final void setFirstColumn(int firstCol) { |
| _firstCol = firstCol; |
| } |
| |
| /** |
| * @param firstRow row number for the upper left hand corner |
| */ |
| public final void setFirstRow(int firstRow) { |
| _firstRow = firstRow; |
| } |
| |
| /** |
| * @param lastCol column number for the lower right hand corner |
| */ |
| public final void setLastColumn(int lastCol) { |
| _lastCol = lastCol; |
| } |
| |
| /** |
| * @param lastRow row number for the lower right hand corner |
| */ |
| public final void setLastRow(int lastRow) { |
| _lastRow = lastRow; |
| } |
| /** |
| * @return the size of the range (number of cells in the area). |
| */ |
| public int getNumberOfCells() { |
| return (_lastRow - _firstRow + 1) * (_lastCol - _firstCol + 1); |
| } |
| |
| /** |
| * Returns an iterator over the CellAddresses in this cell range in row-major order. |
| * @since POI 4.0.0 |
| */ |
| @Override |
| public Iterator<CellAddress> iterator() { |
| return new RowMajorCellAddressIterator(this); |
| } |
| |
| /** |
| * Iterates over the cell addresses in a cell range in row major order |
| * |
| * The iterator is unaffected by changes to the CellRangeAddressBase instance |
| * after the iterator is created. |
| */ |
| private static class RowMajorCellAddressIterator implements Iterator<CellAddress> { |
| private final int firstRow, firstCol, lastRow, lastCol; |
| private int r, c; |
| |
| public RowMajorCellAddressIterator(CellRangeAddressBase ref) { |
| r = firstRow = ref.getFirstRow(); |
| c = firstCol = ref.getFirstColumn(); |
| lastRow = ref.getLastRow(); |
| lastCol = ref.getLastColumn(); |
| |
| // whole row and whole column ranges currently not supported |
| if (firstRow < 0) throw new IllegalStateException("First row cannot be negative."); |
| if (firstCol < 0) throw new IllegalStateException("First column cannot be negative."); |
| |
| // avoid infinite iteration |
| if (firstRow > lastRow) throw new IllegalStateException("First row cannot be greater than last row."); |
| if (firstCol > lastCol) throw new IllegalStateException("First column cannot be greater than last column."); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return r <= lastRow && c <= lastCol; |
| } |
| |
| @Override |
| public CellAddress next() { |
| if (hasNext()) { |
| final CellAddress addr = new CellAddress(r, c); |
| // row major order |
| if (c < lastCol) { |
| c++; |
| } |
| else { //c >= lastCol, end of row reached |
| c = firstCol; //CR |
| r++; //LF |
| } |
| return addr; |
| } |
| throw new NoSuchElementException(); |
| } |
| } |
| |
| @Override |
| public final String toString() { |
| CellAddress crA = new CellAddress(_firstRow, _firstCol); |
| CellAddress crB = new CellAddress(_lastRow, _lastCol); |
| return getClass().getName() + " [" + crA.formatAsString() + ":" + crB.formatAsString() +"]"; |
| } |
| |
| // In case _firstRow > _lastRow or _firstCol > _lastCol |
| protected int getMinRow() { |
| return Math.min(_firstRow, _lastRow); |
| } |
| protected int getMaxRow() { |
| return Math.max(_firstRow, _lastRow); |
| } |
| protected int getMinColumn() { |
| return Math.min(_firstCol, _lastCol); |
| } |
| protected int getMaxColumn() { |
| return Math.max(_firstCol, _lastCol); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other instanceof CellRangeAddressBase) { |
| CellRangeAddressBase o = (CellRangeAddressBase) other; |
| return ((getMinRow() == o.getMinRow()) && |
| (getMaxRow() == o.getMaxRow()) && |
| (getMinColumn() == o.getMinColumn()) && |
| (getMaxColumn() == o.getMaxColumn())); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return (getMinColumn() + |
| (getMaxColumn() << 8) + |
| (getMinRow() << 16) + |
| (getMaxRow() << 24)); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "firstRow", this::getFirstRow, |
| "firstCol", this::getFirstColumn, |
| "lastRow", this::getLastRow, |
| "lastCol", this::getLastColumn |
| ); |
| } |
| } |