All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.spreadsheet.SpreadsheetUtil Maven / Gradle / Ivy

There is a newer version: 24.6.0
Show newest version
/**
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See {@literal } for the full
 * license.
 */
package com.vaadin.flow.component.spreadsheet;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.Format;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.poi.ss.formula.WorkbookEvaluatorProvider;
import org.apache.poi.ss.formula.eval.AreaEval;
import org.apache.poi.ss.formula.eval.RefEval;
import org.apache.poi.ss.formula.eval.RefListEval;
import org.apache.poi.ss.formula.eval.ValueEval;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;

/**
 * Utility class for miscellaneous Spreadsheet operations.
 *
 * @author Vaadin Ltd.
 * @since 1.0
 */
@SuppressWarnings("serial")
public class SpreadsheetUtil implements Serializable {

    private static final Pattern keyParser = Pattern.compile("-?\\d+");

    private static final short EXCEL_COLUMN_WIDTH_FACTOR = 256;

    private static final int UNIT_OFFSET_LENGTH = 7;

    private static final int[] UNIT_OFFSET_MAP = new int[] { 0, 36, 73, 109,
            146, 182, 219 };

    /**
     * Translates cell coordinates to a cell key used to identify cells in the
     * server-client communication.
     *
     * @param col
     *            Column index, 1-based
     * @param row
     *            Row index 1-based
     * @return Cell key
     */
    public static final String toKey(int col, int row) {
        return "col" + col + " row" + row;
    }

    /**
     * Translates cell coordinates from the given Cell object to a cell key used
     * to identify cells in the server-client communication.
     *
     * @param cell
     *            Cell to fetch the coordinates from
     * @return Cell key
     */
    public static final String toKey(Cell cell) {
        return toKey(cell.getColumnIndex() + 1, cell.getRowIndex() + 1);
    }

    /**
     * Determines whether the given cell contains a date or not.
     *
     * @param cell
     *            Cell to examine
     * @return true if the cell contains a date
     */
    public static boolean cellContainsDate(Cell cell) {
        return cell.getCellType() == CellType.NUMERIC
                && DateUtil.isCellDateFormatted(cell);
    }

    public static CellReference relativeToAbsolute(Spreadsheet sheet,
            CellReference cell) {
        String sheetName = sheet.getActiveSheet().getSheetName();
        return new CellReference(sheetName, cell.getRow(), cell.getCol(), true,
                true);
    }

    /**
     * Generates the column header for column with the given index
     *
     * @param columnIndex
     *            Index of column, 1-based
     * @return Generated column header
     */
    public static String getColHeader(int columnIndex) {
        String h = "";
        while (columnIndex > 0) {
            h = (char) ('A' + (columnIndex - 1) % 26) + h;
            columnIndex = (columnIndex - 1) / 26;
        }
        return h;
    }

    /**
     * Returns the column index for the column with the given header.
     *
     * @param header
     *            Column header
     * @return Index of column, 1-based
     */
    public static int getColHeaderIndex(String header) {
        header = header.toUpperCase();
        int x = 0;
        for (int i = 0; i < header.length(); i++) {
            char h = header.charAt(i);
            x = (h - 'A' + 1) + (x * 26);
        }
        return x;
    }

    /**
     * Determines whether the given cell is within the given range.
     *
     * @param cellReference
     *            Target cell reference
     * @param cellRange
     *            Cell range to check
     * @return true if the cell is in the range
     */
    public static boolean isCellInRange(CellReference cellReference,
            CellRangeAddress cellRange) {
        return cellRange.isInRange(cellReference.getRow(),
                cellReference.getCol());
    }

    /**
     * Returns the POI index of the first visible sheet (not hidden or very
     * hidden). If no sheets are visible, returns 0. This is not be possible at
     * least in Excel, but unfortunately POI allows it.
     *
     * @param workbook
     *            Workbook to get the sheets from
     * @return Index of the first visible sheet, 0-based
     */
    public static int getFirstVisibleSheetPOIIndex(Workbook workbook) {
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            if (!(workbook.isSheetHidden(i) && workbook.isSheetVeryHidden(i))) {
                return i;
            }
        }
        return 0;
    }

    /**
     * Returns the number of visible sheets (not hidden or very hidden) in the
     * given Workbook.
     *
     * @param workbook
     *            Workbook to get the sheets from
     * @return Number of visible sheets
     */
    public static int getNumberOfVisibleSheets(Workbook workbook) {
        int result = 0;
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            if (!(workbook.isSheetHidden(i) || workbook.isSheetVeryHidden(i))) {
                result++;
            }
        }
        return result;
    }

    /**
     * Returns the column index for the given Cell key.
     *
     * @param key
     *            Cell key
     * @return Column index of cell, 1-based
     */
    public static int getColumnIndexFromKey(String key) {
        Matcher matcher = keyParser.matcher(key);
        matcher.find(); // find first digit (col)
        return Integer.valueOf(matcher.group());
    }

    /**
     * Returns the row index for the given Cell key.
     *
     * @param key
     *            Cell key
     * @return Row index of cell, 1-based
     */
    public static int getRowFromKey(String key) {
        Matcher matcher = keyParser.matcher(key);
        matcher.find(); // find first digit (col)
        matcher.find(); // find second digit (row)
        return Integer.valueOf(matcher.group());
    }

    /**
     * Converts pixel units to Excel width units (one Excel width unit is
     * 1/256th of a character width)
     *
     * @param pxs
     *            Pixel value to convert
     * @return Value in Excel width units
     */
    static short pixel2WidthUnits(int pxs) {
        short widthUnits = (short) (EXCEL_COLUMN_WIDTH_FACTOR
                * (pxs / UNIT_OFFSET_LENGTH));

        widthUnits += UNIT_OFFSET_MAP[(pxs % UNIT_OFFSET_LENGTH)];

        return widthUnits;
    }

    /**
     * Gets the default column width for new sheets in pixels. The calculation
     * is done using POI.
     *
     * @return Default column width in PX
     */
    static int getDefaultColumnWidthInPx() {
        // Formula taken from XSSFSheet.getColumnWidthInPixels
        return (int) (SpreadsheetFactory.DEFAULT_COL_WIDTH_UNITS
                * EXCEL_COLUMN_WIDTH_FACTOR / 256.0
                * Units.DEFAULT_CHARACTER_WIDTH);
    }

    /**
     * Tries to parse the given String to a percentage. Specifically, checks if
     * the String ends with the '%' character, and the rest can be parsed to a
     * number.
     * 

* * @param cellContent * The string to be parsed * @param locale * The current locale, used for number parsing. * @return the number as a decimal if it can be parsed; e.g. 42% returns * 0.42 and 0.42% returns 0.0042. Returns null if the * number can't be parsed as a decimal. */ public static Double parsePercentage(String cellContent, Locale locale) { if (cellContent == null || cellContent.length() < 2) { return null; } char last = cellContent.charAt(cellContent.length() - 1); if (last == '%') { String sub = cellContent.substring(0, cellContent.length() - 1); Double num = parseNumber(sub, locale); if (num != null) { return num / 100; } } return null; } public static Double parseNumber(Cell cell, String value, Locale locale) { if (value == null || value.trim().isEmpty()) { return null; } if (cell.getCellStyle().getDataFormatString() != null) { DataFormatter df = new CustomDataFormatter(locale); try { Method formatter = df.getClass().getDeclaredMethod("getFormat", Cell.class); formatter.setAccessible(true); Format format = (Format) formatter.invoke(df, cell); if (format != null) { ParsePosition parsePosition = new ParsePosition(0); Object parsed = format.parseObject(value, parsePosition); if (parsePosition.getIndex() == value.length()) { if (parsed instanceof Double) { return (Double) parsed; } else if (parsed instanceof Number) { return ((Number) parsed).doubleValue(); } } } } catch (NoSuchMethodException e) { } catch (InvocationTargetException e) { } catch (IllegalAccessException e) { } catch (UnsupportedOperationException e) { } } return parseNumber(value, locale); } public static Double parseNumber(String cellContent, Locale locale) { if (cellContent == null) { return null; } try { String trimmedContent = cellContent.trim(); if (locale != null) { DecimalFormat format = (DecimalFormat) DecimalFormat .getInstance(locale); DecimalFormatSymbols symbols = format.getDecimalFormatSymbols(); // using format.parse() won't work, as it doesn't handle // grouping separators correctly. If we have an Italian locale // (1.234,00) and try to parse 1.1, the result is 11. So, we // need to do some checking that the grouping separators are in // places we would expect. char groupSep = symbols.getGroupingSeparator(); char decSep = symbols.getDecimalSeparator(); int groupingIndex = trimmedContent.lastIndexOf(groupSep); int decIndex = trimmedContent.indexOf(decSep); // special case; non-breaking space if (groupSep == 160 && groupingIndex == -1) { // try normal space groupSep = ' '; groupingIndex = trimmedContent.lastIndexOf(groupSep); if (groupingIndex != -1) { // replace normal spaces with non-breaking, so that // parsing goes correctly trimmedContent = trimmedContent.replaceAll(" ", " "); } } // no decimal, grouping needs to be 3 characters from end boolean noDecButCorrect = decIndex == -1 && groupingIndex == trimmedContent.length() - 4; // but, if we have scientific notation, the above might not work // (e.g. 4.2e2 has 3 digits, but is invalid). if (groupingIndex != -1) { try { String sub = trimmedContent .substring(trimmedContent.length() - 4); if (sub.toLowerCase().contains("e")) { noDecButCorrect = false; } } catch (IndexOutOfBoundsException e) { // too short for correct use of grouping separator noDecButCorrect = false; } } // decimal point; grouping needs to be 3 chars in front of // decimal point boolean decAndGrouping = groupingIndex + 4 == decIndex; // again, check for scientific notation. If present, E needs to // be after decimal point and not the last char. if (decAndGrouping) { int indexOfE = trimmedContent.toLowerCase().indexOf('e'); if (indexOfE != -1) { if (indexOfE < decIndex || indexOfE == trimmedContent.length() - 1) { decAndGrouping = false; } } } if (groupingIndex == -1 || noDecButCorrect || decAndGrouping) { ParsePosition pos = new ParsePosition(0); Number parse = format.parse(trimmedContent, pos); if (parse != null && pos.getIndex() == trimmedContent.length()) { return parse.doubleValue(); } } } else { // simple check trimmedContent = trimmedContent.replace(",", "."); Double d = Double.parseDouble(trimmedContent); return d; } } catch (NumberFormatException e) { // is OK } return null; } /** * Determine if the given cell content should be displayed with a leading * quote in both cell editor and formula bar *

* * @param cell * The cell to be checked * @return true if the cell contains a string with the "quotePrefix" style * set. Note that for Excel 97 file format, returns true for every * string. */ public static boolean needsLeadingQuote(Cell cell) { if (cell.getCellType() != CellType.STRING) { return false; } if (cell.getStringCellValue() == null) { return false; } return styleHasQuotePrefix(cell); } private static boolean styleHasQuotePrefix(Cell cell) { if (!(cell instanceof XSSFCell)) { // in 97 format all strings are prefixed return true; } XSSFCellStyle cellStyle = (XSSFCellStyle) cell.getCellStyle(); if (cellStyle == null || cellStyle.getCoreXf() == null) { return false; } return cellStyle.getCoreXf().getQuotePrefix(); } /** * evaluate the formula (which may just be a single cell or range string) * and find the bounding rectangle for the referenced cells. * * @param formula * @param spreadsheet * @param includeHiddenCells * @return CellRangeAddress bounding the evaluated result */ public static CellRangeAddress getRangeForReference(String formula, Spreadsheet spreadsheet, boolean includeHiddenCells) { int minRow = Integer.MAX_VALUE; int minCol = Integer.MAX_VALUE; int maxRow = 0; int maxCol = 0; for (CellReference ref : getAllReferencedCells(formula, spreadsheet, includeHiddenCells)) { minRow = Math.min(minRow, ref.getRow()); maxRow = Math.max(maxRow, ref.getRow()); minCol = Math.min(minCol, ref.getCol()); maxCol = Math.max(maxCol, ref.getCol()); } return new CellRangeAddress(minRow, maxRow, minCol, maxCol); } /** * This function returns all the cells that the given formula references. * You can optionally filter out all the hidden rows from the list honoring * filtering of charts based on SpreadsheetTable filter settings. * * @param formula * The formula to find referenced cells for * @param spreadsheet * Spreadsheet to operate on * @param includeHiddenCells * true to include cells residing in hidden rows or * columns, false to omit them * */ public static List getAllReferencedCells(String formula, Spreadsheet spreadsheet, boolean includeHiddenCells) { final List cellRefs = new ArrayList<>(); getAllReferencedCells( ((WorkbookEvaluatorProvider) spreadsheet.getFormulaEvaluator()) ._getWorkbookEvaluator().evaluate(formula, new CellReference( spreadsheet.getActiveSheet() .getSheetName(), 0, 0, false, false)), spreadsheet, cellRefs); if (includeHiddenCells) { return cellRefs; } else { // Filter out hidden cells of rows that are hidden (Excel spec) ArrayList visibleCells = new ArrayList(); for (CellReference cr : cellRefs) { if (!spreadsheet.isRowHidden(cr.getRow()) && !spreadsheet.isColumnHidden(cr.getCol())) { visibleCells.add(cr); } } return visibleCells; } } private static void getAllReferencedCells(ValueEval rawEval, Spreadsheet spreadsheet, List cells) { if (rawEval instanceof AreaEval) { // includes 2D and 3D contiguous ranges (1+ sheets, start/end // row/column) final AreaEval areaEval = (AreaEval) rawEval; for (int s = areaEval.getFirstSheetIndex(); s <= areaEval .getLastSheetIndex(); s++) { for (int r = areaEval.getFirstRow(); r <= areaEval .getLastRow(); r++) { for (int c = areaEval.getFirstColumn(); c <= areaEval .getLastColumn(); c++) { cells.add(new CellReference( spreadsheet.getWorkbook().getSheetName(s), r, c, false, false)); } } } } else if (rawEval instanceof RefEval) { // same cell on 1+ sheets, by row/column index final RefEval refEval = (RefEval) rawEval; for (int s = refEval.getFirstSheetIndex(); s <= refEval .getLastSheetIndex(); s++) { cells.add(new CellReference( spreadsheet.getWorkbook().getSheetName(s), refEval.getRow(), refEval.getColumn(), false, false)); } } else if (rawEval instanceof RefListEval) { // list of evals, call this with each one final RefListEval list = (RefListEval) rawEval; for (ValueEval eval : list.getList()) getAllReferencedCells(eval, spreadsheet, cells); } // ignore others, static values, not cell references } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy