Skip to content

Commit 2b06134

Browse files
authored
Merge pull request #454 from civgio/hotfix/issue_453
#453: Add new data validation: ListFormulaDataValidation.
2 parents 52fb605 + 12e2edd commit 2b06134

File tree

4 files changed

+283
-31
lines changed

4 files changed

+283
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package org.dhatim.fastexcel;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* A ListDataValidation defines a DataValidation for a worksheet of type = "list"
7+
*/
8+
public class ListFormulaDataValidation implements DataValidation {
9+
private final static String TYPE = "list";
10+
private final Range range;
11+
private final Formula formula;
12+
13+
private boolean allowBlank = true;
14+
private boolean showDropdown = true;
15+
private DataValidationErrorStyle errorStyle = DataValidationErrorStyle.INFORMATION;
16+
private boolean showErrorMessage = false;
17+
private String errorTitle;
18+
private String error;
19+
20+
/**
21+
* Constructor
22+
*
23+
* @param range The Range this validation is applied to
24+
* @param formula The Formula of this validation to retrieve the list
25+
*/
26+
ListFormulaDataValidation(Range range, Formula formula) {
27+
this.range = range;
28+
this.formula = formula;
29+
}
30+
31+
/**
32+
* whether blank cells should pass the validation
33+
*
34+
* @param allowBlank whether or not to allow blank values
35+
* @return this ListDataValidation
36+
*/
37+
public ListFormulaDataValidation allowBlank(boolean allowBlank) {
38+
this.allowBlank = allowBlank;
39+
return this;
40+
}
41+
42+
/**
43+
* Whether Excel will show an in-cell dropdown list
44+
* containing the validation list
45+
*
46+
* @param showDropdown whether or not to show the dropdown
47+
* @return this ListDataValidation
48+
*/
49+
public ListFormulaDataValidation showDropdown(boolean showDropdown) {
50+
this.showDropdown = showDropdown;
51+
return this;
52+
}
53+
54+
/**
55+
* The style of error alert used for this data validation.
56+
*
57+
* @param errorStyle The DataValidationErrorStyle for this DataValidation
58+
* @return this ListDataValidation
59+
*/
60+
public ListFormulaDataValidation errorStyle(DataValidationErrorStyle errorStyle) {
61+
this.errorStyle = errorStyle;
62+
return this;
63+
}
64+
65+
/**
66+
* Whether to display the error alert message when an invalid value has been entered.
67+
*
68+
* @param showErrorMessage whether to display the error message
69+
* @return this ListDataValidation
70+
*/
71+
public ListFormulaDataValidation showErrorMessage(boolean showErrorMessage) {
72+
this.showErrorMessage = showErrorMessage;
73+
return this;
74+
}
75+
76+
/**
77+
* Title bar text of error alert.
78+
*
79+
* @param errorTitle The error title
80+
* @return this ListDataValidation
81+
*/
82+
public ListFormulaDataValidation errorTitle(String errorTitle) {
83+
this.errorTitle = errorTitle;
84+
return this;
85+
}
86+
87+
/**
88+
* Message text of error alert.
89+
*
90+
* @param error The error message
91+
* @return this ListDataValidation
92+
*/
93+
public ListFormulaDataValidation error(String error) {
94+
this.error = error;
95+
return this;
96+
}
97+
98+
/**
99+
* Write this dataValidation as an XML element.
100+
*
101+
* @param w Output writer.
102+
* @throws IOException If an I/O error occurs.
103+
*/
104+
@Override
105+
public void write(Writer w) throws IOException {
106+
w
107+
.append("<dataValidation sqref=\"")
108+
.append(range.toString())
109+
.append("\" type=\"")
110+
.append(TYPE)
111+
.append("\" allowBlank=\"")
112+
.append(String.valueOf(allowBlank))
113+
.append("\" showDropDown=\"")
114+
.append(String.valueOf(!showDropdown)) // for some reason, this is the inverse of what you'd expect
115+
.append("\" errorStyle=\"")
116+
.append(errorStyle.toString())
117+
.append("\" showErrorMessage=\"")
118+
.append(String.valueOf(showErrorMessage))
119+
.append("\" errorTitle=\"")
120+
.append(errorTitle)
121+
.append("\" error=\"")
122+
.append(error)
123+
.append("\"><formula1>")
124+
.append(formula.getExpression())
125+
.append("</formula1></dataValidation>");
126+
}
127+
}

Diff for: fastexcel-writer/src/main/java/org/dhatim/fastexcel/Range.java

+37-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public class Range implements Ref {
4444
*/
4545
private final int right;
4646

47+
/**
48+
* enable the folder scope when this range is added to a worksheet's named ranges
49+
*/
50+
private boolean folderScope = false;
51+
4752
/**
4853
* Constructor. Note coordinates are reordered if necessary to make sure
4954
* {@code top} &lt;= {@code bottom} and {@code left} &lt;= {@code right}.
@@ -201,7 +206,19 @@ public ListDataValidation validateWithList(Range listRange) {
201206
return listDataValidation;
202207
}
203208

204-
/**
209+
/**
210+
* Construct a new ListDataValidation
211+
*
212+
* @param formula The Formula to retrieve the validation list
213+
* @return a new list data validation object
214+
*/
215+
public ListFormulaDataValidation validateWithListByFormula(String formula) {
216+
ListFormulaDataValidation listDataValidation = new ListFormulaDataValidation(this, new Formula(formula));
217+
worksheet.addValidation(listDataValidation);
218+
return listDataValidation;
219+
}
220+
221+
/**
205222
* Construct a new ListDataValidation
206223
*
207224
* @param formula The custom validation formula
@@ -216,13 +233,31 @@ public CustomDataValidation validateWithFormula(String formula) {
216233
/**
217234
* Specifically define this range by assigning it a name.
218235
* It will be visible in the cell range dropdown menu.
219-
*
236+
*
220237
* @param name string representing the name of this cell range
221238
*/
222239
public void setName(String name) {
223240
worksheet.addNamedRange(this, name);
224241
}
225242

243+
/**
244+
* Check if this range has a folder scope. It is used by {@link Worksheet#addNamedRange(Range, String)}.
245+
*
246+
* @return {@code true} if the range has a folder scope, {@code false} if it is visible only by the worksheet contains the range
247+
*/
248+
public boolean isFolderScope() {
249+
return folderScope;
250+
}
251+
252+
/**
253+
* Set the visibility of this range
254+
*
255+
* @param folderScope {@code true} to allow to see the range by all worksheet
256+
*/
257+
public void setFolderScope(boolean folderScope) {
258+
this.folderScope = folderScope;
259+
}
260+
226261
/**
227262
* Return the set of styles used by the cells in this range.
228263
*

Diff for: fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java

+33-29
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,14 @@ private boolean hasComments() {
291291
private void writeWorkbookFile() throws IOException {
292292
writeFile("xl/workbook.xml", w -> {
293293
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
294-
"<workbook " +
295-
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
296-
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
297-
"<workbookPr date1904=\"false\"/>" +
298-
"<bookViews>" +
299-
"<workbookView activeTab=\"" + activeTab + "\"/>" +
300-
"</bookViews>" +
301-
"<sheets>");
294+
"<workbook " +
295+
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
296+
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
297+
"<workbookPr date1904=\"false\"/>" +
298+
"<bookViews>" +
299+
"<workbookView activeTab=\"" + activeTab + "\"/>" +
300+
"</bookViews>" +
301+
"<sheets>");
302302

303303
for (Worksheet ws : worksheets) {
304304
writeWorkbookSheet(w, ws);
@@ -312,12 +312,12 @@ private void writeWorkbookFile() throws IOException {
312312
for (Worksheet ws : worksheets) {
313313
int worksheetIndex = getIndex(ws) - 1;
314314
List<Object> repeatingColsAndRows = Stream.of(ws.getRepeatingCols(), ws.getRepeatingRows())
315-
.filter(Objects::nonNull)
316-
.collect(Collectors.toList());
315+
.filter(Objects::nonNull)
316+
.collect(Collectors.toList());
317317
if (!repeatingColsAndRows.isEmpty()) {
318318
w.append("<definedName function=\"false\" hidden=\"false\" localSheetId=\"")
319-
.append(worksheetIndex)
320-
.append("\" name=\"_xlnm.Print_Titles\" vbProcedure=\"false\">");
319+
.append(worksheetIndex)
320+
.append("\" name=\"_xlnm.Print_Titles\" vbProcedure=\"false\">");
321321
for (int i = 0; i < repeatingColsAndRows.size(); ++i) {
322322
if (i > 0) {
323323
w.append(",");
@@ -330,26 +330,30 @@ private void writeWorkbookFile() throws IOException {
330330
for (Map.Entry<String, Range> nr : ws.getNamedRanges().entrySet()) {
331331
String rangeName = nr.getKey();
332332
Range range = nr.getValue();
333-
w.append("<definedName function=\"false\" " +
334-
"hidden=\"false\" localSheetId=\"")
335-
.append(worksheetIndex)
336-
.append("\" name=\"")
337-
.append(rangeName)
338-
.append("\" vbProcedure=\"false\">'")
339-
.appendEscaped(ws.getName())
340-
.append("'!")
341-
.append(range.toAbsoluteString())
342-
.append("</definedName>");
333+
w.append("<definedName function=\"false\" hidden=\"false\"");
334+
335+
if (!range.isFolderScope()) {
336+
w.append(" localSheetId=\"")
337+
.append(worksheetIndex).append("\"");
338+
}
339+
340+
w.append(" name=\"")
341+
.append(rangeName)
342+
.append("\" vbProcedure=\"false\">'")
343+
.appendEscaped(ws.getName())
344+
.append("'!")
345+
.append(range.toAbsoluteString())
346+
.append("</definedName>");
343347
}
344348
Range af = ws.getAutoFilterRange();
345349
if (af != null) {
346350
w.append("<definedName function=\"false\" hidden=\"true\" localSheetId=\"")
347-
.append(worksheetIndex)
348-
.append("\" name=\"_xlnm._FilterDatabase\" vbProcedure=\"false\">'")
349-
.appendEscaped(ws.getName())
350-
.append("'!")
351-
.append(af.toAbsoluteString())
352-
.append("</definedName>");
351+
.append(worksheetIndex)
352+
.append("\" name=\"_xlnm._FilterDatabase\" vbProcedure=\"false\">'")
353+
.appendEscaped(ws.getName())
354+
.append("'!")
355+
.append(af.toAbsoluteString())
356+
.append("</definedName>");
353357
}
354358
}
355359
w.append("</definedNames>");
@@ -366,7 +370,7 @@ private void writeWorkbookFile() throws IOException {
366370
*/
367371
private void writeWorkbookSheet(Writer w, Worksheet ws) throws IOException {
368372
w.append("<sheet name=\"").appendEscaped(ws.getName()).append("\" r:id=\"rId").append(getIndex(ws) + 2)
369-
.append("\" sheetId=\"").append(getIndex(ws));
373+
.append("\" sheetId=\"").append(getIndex(ws));
370374

371375
if (ws.getVisibilityState() != null) {
372376
w.append("\" state=\"").append(ws.getVisibilityState().getName());

Diff for: fastexcel-writer/src/test/java/org/dhatim/fastexcel/PoiCompatibilityTest.java

+86
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.dhatim.fastexcel;
22

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

423+
@Test
424+
void listFormulaValidations() throws Exception {
425+
426+
String errMsg = "Error Message";
427+
String errTitle = "Error Title";
428+
429+
String mainWorksheet = "Worksheet 1";
430+
String worksheetWithListValues = "Lists";
431+
432+
// name of named range to add
433+
String namedRange = "VALUES";
434+
435+
byte[] data = writeWorkbook(wb -> {
436+
Worksheet ws = wb.newWorksheet(mainWorksheet);
437+
438+
// add list of values
439+
Worksheet listWs = wb.newWorksheet(worksheetWithListValues);
440+
listWs.value(0, 0, "val1");
441+
listWs.value(1, 0, "val2");
442+
443+
// hidden worksheet with values
444+
listWs.setVisibilityState(VisibilityState.HIDDEN);
445+
446+
Range listRange = listWs.range(0, 0, 1, 0);
447+
// the folder scope to the range allows to show the "named range" if the list worksheet is hidden too
448+
listRange.setFolderScope(true);
449+
listWs.addNamedRange(listRange, namedRange);
450+
451+
452+
// add cell with name of "named range" to retrieve
453+
ws.value(0, 0, "VALUES");
454+
455+
String formula = "INDIRECT($A$1)";
456+
457+
ListFormulaDataValidation listFormulaDataValidation = ws.range(0, 1, 100, 1).validateWithListByFormula(formula);
458+
listFormulaDataValidation
459+
.allowBlank(false)
460+
.error(errMsg)
461+
.errorTitle(errTitle)
462+
.errorStyle(DataValidationErrorStyle.WARNING)
463+
.showErrorMessage(true);
464+
});
465+
466+
// Check generated workbook with Apache POI
467+
XSSFWorkbook xwb = new XSSFWorkbook(new ByteArrayInputStream(data));
468+
assertThat(xwb.getNumberOfSheets()).isEqualTo(2);
469+
470+
XSSFSheet xwsValues = xwb.getSheet(worksheetWithListValues);
471+
// check visibility of sheet contains value list
472+
assertThat(xwb.getSheetVisibility(xwb.getSheetIndex(xwsValues))).isEqualTo(SheetVisibility.HIDDEN);
473+
474+
// check the named range and its reference
475+
XSSFName xssfName = xwb.getName(namedRange);
476+
xssfName.getRefersToFormula().equals("'Lists'!$A$1:$A$2");
477+
// check that the named range has a global scope and not a reference to the local sheet
478+
assertThat(xssfName.getSheetIndex()).isEqualTo(-1);
479+
480+
XSSFSheet xws = xwb.getSheet(mainWorksheet);
481+
482+
// check number of data validation of main worksheet
483+
assertThat(xws.getDataValidations().size()).isEqualTo(1);
484+
485+
XSSFDataValidation dataValidation = xws.getDataValidations().get(0);
486+
487+
assertThat(dataValidation.getEmptyCellAllowed()).isFalse();
488+
assertThat(dataValidation.getErrorBoxText()).isEqualTo(errMsg);
489+
assertThat(dataValidation.getErrorBoxTitle()).isEqualTo(errTitle);
490+
assertThat(dataValidation.getErrorStyle()).isEqualTo(ErrorStyle.WARNING);
491+
assertThat(dataValidation.getShowErrorBox()).isTrue();
492+
assertThat(dataValidation.getSuppressDropDownArrow()).isTrue();
493+
assertThat(dataValidation.getRegions().getCellRangeAddresses().length).isEqualTo(1);
494+
495+
CellRangeAddress cellRangeAddress = dataValidation.getRegions().getCellRangeAddress(0);
496+
assertThat(cellRangeAddress.getFirstColumn()).isEqualTo(1);
497+
assertThat(cellRangeAddress.getLastColumn()).isEqualTo(1);
498+
assertThat(cellRangeAddress.getFirstRow()).isEqualTo(0);
499+
assertThat(cellRangeAddress.getLastRow()).isEqualTo(100);
500+
501+
502+
DataValidationConstraint validationConstraint = dataValidation.getValidationConstraint();
503+
assertThat(validationConstraint.getFormula1().toLowerCase()).isEqualToIgnoringCase("INDIRECT($A$1)");
504+
assertThat(validationConstraint.getValidationType()).isEqualTo(DataValidationConstraint.ValidationType.LIST);
505+
506+
}
507+
422508
@Test
423509
void canHideSheet() throws IOException {
424510

0 commit comments

Comments
 (0)