Skip to content

Commit

Permalink
Merge pull request #454 from civgio/hotfix/issue_453
Browse files Browse the repository at this point in the history
#453: Add new data validation: ListFormulaDataValidation.
  • Loading branch information
rzymek committed Sep 6, 2024
2 parents 52fb605 + 12e2edd commit 2b06134
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.dhatim.fastexcel;

import java.io.IOException;

/**
* A ListDataValidation defines a DataValidation for a worksheet of type = "list"
*/
public class ListFormulaDataValidation implements DataValidation {
private final static String TYPE = "list";
private final Range range;
private final Formula formula;

private boolean allowBlank = true;
private boolean showDropdown = true;
private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION;
private boolean showErrorMessage = false;
private String errorTitle;
private String error;

/**
* Constructor
*
* @param range The Range this validation is applied to
* @param formula The Formula of this validation to retrieve the list
*/
ListFormulaDataValidation(Range range, Formula formula) {
this.range = range;
this.formula = formula;
}

/**
* whether blank cells should pass the validation
*
* @param allowBlank whether or not to allow blank values
* @return this ListDataValidation
*/
public ListFormulaDataValidation allowBlank(boolean allowBlank) {
this.allowBlank = allowBlank;
return this;
}

/**
* Whether Excel will show an in-cell dropdown list
* containing the validation list
*
* @param showDropdown whether or not to show the dropdown
* @return this ListDataValidation
*/
public ListFormulaDataValidation showDropdown(boolean showDropdown) {
this.showDropdown = showDropdown;
return this;
}

/**
* The style of error alert used for this data validation.
*
* @param errorStyle The DataValidationErrorStyle for this DataValidation
* @return this ListDataValidation
*/
public ListFormulaDataValidation errorStyle(DataValidationErrorStyle errorStyle) {
this.errorStyle = errorStyle;
return this;
}

/**
* Whether to display the error alert message when an invalid value has been entered.
*
* @param showErrorMessage whether to display the error message
* @return this ListDataValidation
*/
public ListFormulaDataValidation showErrorMessage(boolean showErrorMessage) {
this.showErrorMessage = showErrorMessage;
return this;
}

/**
* Title bar text of error alert.
*
* @param errorTitle The error title
* @return this ListDataValidation
*/
public ListFormulaDataValidation errorTitle(String errorTitle) {
this.errorTitle = errorTitle;
return this;
}

/**
* Message text of error alert.
*
* @param error The error message
* @return this ListDataValidation
*/
public ListFormulaDataValidation error(String error) {
this.error = error;
return this;
}

/**
* Write this dataValidation as an XML element.
*
* @param w Output writer.
* @throws IOException If an I/O error occurs.
*/
@Override
public void write(Writer w) throws IOException {
w
.append("<dataValidation sqref=\"")
.append(range.toString())
.append("\" type=\"")
.append(TYPE)
.append("\" allowBlank=\"")
.append(String.valueOf(allowBlank))
.append("\" showDropDown=\"")
.append(String.valueOf(!showDropdown)) // for some reason, this is the inverse of what you'd expect
.append("\" errorStyle=\"")
.append(errorStyle.toString())
.append("\" showErrorMessage=\"")
.append(String.valueOf(showErrorMessage))
.append("\" errorTitle=\"")
.append(errorTitle)
.append("\" error=\"")
.append(error)
.append("\"><formula1>")
.append(formula.getExpression())
.append("</formula1></dataValidation>");
}
}
39 changes: 37 additions & 2 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Range.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public class Range implements Ref {
*/
private final int right;

/**
* enable the folder scope when this range is added to a worksheet's named ranges
*/
private boolean folderScope = false;

/**
* Constructor. Note coordinates are reordered if necessary to make sure
* {@code top} &lt;= {@code bottom} and {@code left} &lt;= {@code right}.
Expand Down Expand Up @@ -201,7 +206,19 @@ public ListDataValidation validateWithList(Range listRange) {
return listDataValidation;
}

/**
/**
* Construct a new ListDataValidation
*
* @param formula The Formula to retrieve the validation list
* @return a new list data validation object
*/
public ListFormulaDataValidation validateWithListByFormula(String formula) {
ListFormulaDataValidation listDataValidation = new ListFormulaDataValidation(this, new Formula(formula));
worksheet.addValidation(listDataValidation);
return listDataValidation;
}

/**
* Construct a new ListDataValidation
*
* @param formula The custom validation formula
Expand All @@ -216,13 +233,31 @@ public CustomDataValidation validateWithFormula(String formula) {
/**
* Specifically define this range by assigning it a name.
* It will be visible in the cell range dropdown menu.
*
*
* @param name string representing the name of this cell range
*/
public void setName(String name) {
worksheet.addNamedRange(this, name);
}

/**
* Check if this range has a folder scope. It is used by {@link Worksheet#addNamedRange(Range, String)}.
*
* @return {@code true} if the range has a folder scope, {@code false} if it is visible only by the worksheet contains the range
*/
public boolean isFolderScope() {
return folderScope;
}

/**
* Set the visibility of this range
*
* @param folderScope {@code true} to allow to see the range by all worksheet
*/
public void setFolderScope(boolean folderScope) {
this.folderScope = folderScope;
}

/**
* Return the set of styles used by the cells in this range.
*
Expand Down
62 changes: 33 additions & 29 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,14 @@ private boolean hasComments() {
private void writeWorkbookFile() throws IOException {
writeFile("xl/workbook.xml", w -> {
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<workbook " +
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<workbookPr date1904=\"false\"/>" +
"<bookViews>" +
"<workbookView activeTab=\"" + activeTab + "\"/>" +
"</bookViews>" +
"<sheets>");
"<workbook " +
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<workbookPr date1904=\"false\"/>" +
"<bookViews>" +
"<workbookView activeTab=\"" + activeTab + "\"/>" +
"</bookViews>" +
"<sheets>");

for (Worksheet ws : worksheets) {
writeWorkbookSheet(w, ws);
Expand All @@ -312,12 +312,12 @@ private void writeWorkbookFile() throws IOException {
for (Worksheet ws : worksheets) {
int worksheetIndex = getIndex(ws) - 1;
List<Object> repeatingColsAndRows = Stream.of(ws.getRepeatingCols(), ws.getRepeatingRows())
.filter(Objects::nonNull)
.collect(Collectors.toList());
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!repeatingColsAndRows.isEmpty()) {
w.append("<definedName function=\"false\" hidden=\"false\" localSheetId=\"")
.append(worksheetIndex)
.append("\" name=\"_xlnm.Print_Titles\" vbProcedure=\"false\">");
.append(worksheetIndex)
.append("\" name=\"_xlnm.Print_Titles\" vbProcedure=\"false\">");
for (int i = 0; i < repeatingColsAndRows.size(); ++i) {
if (i > 0) {
w.append(",");
Expand All @@ -330,26 +330,30 @@ private void writeWorkbookFile() throws IOException {
for (Map.Entry<String, Range> nr : ws.getNamedRanges().entrySet()) {
String rangeName = nr.getKey();
Range range = nr.getValue();
w.append("<definedName function=\"false\" " +
"hidden=\"false\" localSheetId=\"")
.append(worksheetIndex)
.append("\" name=\"")
.append(rangeName)
.append("\" vbProcedure=\"false\">'")
.appendEscaped(ws.getName())
.append("'!")
.append(range.toAbsoluteString())
.append("</definedName>");
w.append("<definedName function=\"false\" hidden=\"false\"");

if (!range.isFolderScope()) {
w.append(" localSheetId=\"")
.append(worksheetIndex).append("\"");
}

w.append(" name=\"")
.append(rangeName)
.append("\" vbProcedure=\"false\">'")
.appendEscaped(ws.getName())
.append("'!")
.append(range.toAbsoluteString())
.append("</definedName>");
}
Range af = ws.getAutoFilterRange();
if (af != null) {
w.append("<definedName function=\"false\" hidden=\"true\" localSheetId=\"")
.append(worksheetIndex)
.append("\" name=\"_xlnm._FilterDatabase\" vbProcedure=\"false\">'")
.appendEscaped(ws.getName())
.append("'!")
.append(af.toAbsoluteString())
.append("</definedName>");
.append(worksheetIndex)
.append("\" name=\"_xlnm._FilterDatabase\" vbProcedure=\"false\">'")
.appendEscaped(ws.getName())
.append("'!")
.append(af.toAbsoluteString())
.append("</definedName>");
}
}
w.append("</definedNames>");
Expand All @@ -366,7 +370,7 @@ private void writeWorkbookFile() throws IOException {
*/
private void writeWorkbookSheet(Writer w, Worksheet ws) throws IOException {
w.append("<sheet name=\"").appendEscaped(ws.getName()).append("\" r:id=\"rId").append(getIndex(ws) + 2)
.append("\" sheetId=\"").append(getIndex(ws));
.append("\" sheetId=\"").append(getIndex(ws));

if (ws.getVisibilityState() != null) {
w.append("\" state=\"").append(ws.getVisibilityState().getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.dhatim.fastexcel;

import org.apache.poi.ss.formula.WorkbookEvaluator;
import org.apache.poi.ss.usermodel.DataValidation.ErrorStyle;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
Expand Down Expand Up @@ -419,6 +420,91 @@ void listValidations() throws Exception {
assertThat(validationConstraint.getFormula1().toLowerCase()).isEqualToIgnoringCase("'Lists'!$A$1:$A$2");
}

@Test
void listFormulaValidations() throws Exception {

String errMsg = "Error Message";
String errTitle = "Error Title";

String mainWorksheet = "Worksheet 1";
String worksheetWithListValues = "Lists";

// name of named range to add
String namedRange = "VALUES";

byte[] data = writeWorkbook(wb -> {
Worksheet ws = wb.newWorksheet(mainWorksheet);

// add list of values
Worksheet listWs = wb.newWorksheet(worksheetWithListValues);
listWs.value(0, 0, "val1");
listWs.value(1, 0, "val2");

// hidden worksheet with values
listWs.setVisibilityState(VisibilityState.HIDDEN);

Range listRange = listWs.range(0, 0, 1, 0);
// the folder scope to the range allows to show the "named range" if the list worksheet is hidden too
listRange.setFolderScope(true);
listWs.addNamedRange(listRange, namedRange);


// add cell with name of "named range" to retrieve
ws.value(0, 0, "VALUES");

String formula = "INDIRECT($A$1)";

ListFormulaDataValidation listFormulaDataValidation = ws.range(0, 1, 100, 1).validateWithListByFormula(formula);
listFormulaDataValidation
.allowBlank(false)
.error(errMsg)
.errorTitle(errTitle)
.errorStyle(DataValidationErrorStyle.WARNING)
.showErrorMessage(true);
});

// Check generated workbook with Apache POI
XSSFWorkbook xwb = new XSSFWorkbook(new ByteArrayInputStream(data));
assertThat(xwb.getNumberOfSheets()).isEqualTo(2);

XSSFSheet xwsValues = xwb.getSheet(worksheetWithListValues);
// check visibility of sheet contains value list
assertThat(xwb.getSheetVisibility(xwb.getSheetIndex(xwsValues))).isEqualTo(SheetVisibility.HIDDEN);

// check the named range and its reference
XSSFName xssfName = xwb.getName(namedRange);
xssfName.getRefersToFormula().equals("'Lists'!$A$1:$A$2");
// check that the named range has a global scope and not a reference to the local sheet
assertThat(xssfName.getSheetIndex()).isEqualTo(-1);

XSSFSheet xws = xwb.getSheet(mainWorksheet);

// check number of data validation of main worksheet
assertThat(xws.getDataValidations().size()).isEqualTo(1);

XSSFDataValidation dataValidation = xws.getDataValidations().get(0);

assertThat(dataValidation.getEmptyCellAllowed()).isFalse();
assertThat(dataValidation.getErrorBoxText()).isEqualTo(errMsg);
assertThat(dataValidation.getErrorBoxTitle()).isEqualTo(errTitle);
assertThat(dataValidation.getErrorStyle()).isEqualTo(ErrorStyle.WARNING);
assertThat(dataValidation.getShowErrorBox()).isTrue();
assertThat(dataValidation.getSuppressDropDownArrow()).isTrue();
assertThat(dataValidation.getRegions().getCellRangeAddresses().length).isEqualTo(1);

CellRangeAddress cellRangeAddress = dataValidation.getRegions().getCellRangeAddress(0);
assertThat(cellRangeAddress.getFirstColumn()).isEqualTo(1);
assertThat(cellRangeAddress.getLastColumn()).isEqualTo(1);
assertThat(cellRangeAddress.getFirstRow()).isEqualTo(0);
assertThat(cellRangeAddress.getLastRow()).isEqualTo(100);


DataValidationConstraint validationConstraint = dataValidation.getValidationConstraint();
assertThat(validationConstraint.getFormula1().toLowerCase()).isEqualToIgnoringCase("INDIRECT($A$1)");
assertThat(validationConstraint.getValidationType()).isEqualTo(DataValidationConstraint.ValidationType.LIST);

}

@Test
void canHideSheet() throws IOException {

Expand Down

0 comments on commit 2b06134

Please sign in to comment.